diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index 58a198808b91..7a2f2174546f 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -2906,6 +2906,11 @@ checksums: workspace/surveys/edit/checkbox_label: 12a07d6bdf38e283a2e95892ec49b7f8 workspace/surveys/edit/choose_the_actions_which_trigger_the_survey: 773b311a148a112243f3b139506b9987 workspace/surveys/edit/choose_the_first_question_on_your_block: bdece06ca04f89d0c445ba1554dd5b80 + workspace/surveys/edit/element_category_input: c281c28cbb062bc3538cbd4a42d79cf6 + workspace/surveys/edit/element_category_choice: 307101bb385166b538e9370428963461 + workspace/surveys/edit/element_category_scoring: 29dbcaf797b1e9f985bbd7b5258749e4 + workspace/surveys/edit/element_category_contact: 9afa39bc47019ee6dec6c74b6273967c + workspace/surveys/edit/element_category_content: 669b337031ef165af577f04f2b7fd54f workspace/surveys/edit/choose_where_to_run_the_survey: ad87bcae97c445f1fd9ac110ea24f117 workspace/surveys/edit/city: 1831f32e1babbb29af27fac3053504a2 workspace/surveys/edit/clear_close_on_date: 673ed6940f36b02cf871ffacf034e114 diff --git a/apps/web/lib/posthog/capture.test.ts b/apps/web/lib/posthog/capture.test.ts index f7ecb4838546..d5b679ed4e80 100644 --- a/apps/web/lib/posthog/capture.test.ts +++ b/apps/web/lib/posthog/capture.test.ts @@ -1,9 +1,10 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; -import { capturePostHogEvent, groupIdentifyPostHog } from "./capture"; +import { capturePostHogEvent, groupIdentifyPostHog, identifyPostHogPerson } from "./capture"; const mocks = vi.hoisted(() => ({ capture: vi.fn(), groupIdentify: vi.fn(), + identify: vi.fn(), loggerWarn: vi.fn(), })); @@ -14,7 +15,11 @@ vi.mock("@formbricks/logger", () => ({ })); vi.mock("./server", () => ({ - posthogServerClient: { capture: mocks.capture, groupIdentify: mocks.groupIdentify }, + posthogServerClient: { + capture: mocks.capture, + groupIdentify: mocks.groupIdentify, + identify: mocks.identify, + }, })); describe("capturePostHogEvent", () => { @@ -136,6 +141,42 @@ describe("groupIdentifyPostHog", () => { }); }); +describe("identifyPostHogPerson", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test("calls posthog identify with distinctId and properties", () => { + identifyPostHogPerson("user123", { email: "user@example.com", name: "Ada" }); + + expect(mocks.identify).toHaveBeenCalledWith({ + distinctId: "user123", + properties: { email: "user@example.com", name: "Ada" }, + }); + }); + + test("passes undefined properties when none provided", () => { + identifyPostHogPerson("user123"); + + expect(mocks.identify).toHaveBeenCalledWith({ + distinctId: "user123", + properties: undefined, + }); + }); + + test("does not throw when identify throws", () => { + mocks.identify.mockImplementation(() => { + throw new Error("Network error"); + }); + + expect(() => identifyPostHogPerson("user123", { email: "user@example.com" })).not.toThrow(); + expect(mocks.loggerWarn).toHaveBeenCalledWith( + { error: expect.any(Error) }, + "Failed to identify PostHog person" + ); + }); +}); + describe("capturePostHogEvent with null client", () => { test("no-ops when posthogServerClient is null", async () => { vi.clearAllMocks(); @@ -149,14 +190,19 @@ describe("capturePostHogEvent with null client", () => { posthogServerClient: null, })); - const { capturePostHogEvent: captureWithNullClient, groupIdentifyPostHog: identifyWithNullClient } = - await import("./capture"); + const { + capturePostHogEvent: captureWithNullClient, + groupIdentifyPostHog: identifyWithNullClient, + identifyPostHogPerson: identifyPersonWithNullClient, + } = await import("./capture"); captureWithNullClient("user123", "test_event", { key: "value" }); identifyWithNullClient("organization", "org_1"); + identifyPersonWithNullClient("user123", { email: "user@example.com" }); expect(mocks.capture).not.toHaveBeenCalled(); expect(mocks.groupIdentify).not.toHaveBeenCalled(); + expect(mocks.identify).not.toHaveBeenCalled(); expect(mocks.loggerWarn).not.toHaveBeenCalled(); }); }); diff --git a/apps/web/lib/posthog/capture.ts b/apps/web/lib/posthog/capture.ts index 23eb5057ce37..b9870139209e 100644 --- a/apps/web/lib/posthog/capture.ts +++ b/apps/web/lib/posthog/capture.ts @@ -41,6 +41,16 @@ export function capturePostHogEvent( } } +export function identifyPostHogPerson(distinctId: string, properties?: PostHogEventProperties): void { + if (!posthogServerClient) return; + + try { + posthogServerClient.identify({ distinctId, properties }); + } catch (error) { + logger.warn({ error }, "Failed to identify PostHog person"); + } +} + type PostHogGroupType = "organization" | "workspace"; export function groupIdentifyPostHog( diff --git a/apps/web/lib/posthog/index.ts b/apps/web/lib/posthog/index.ts index 9824359267e7..eb6eac0727fd 100644 --- a/apps/web/lib/posthog/index.ts +++ b/apps/web/lib/posthog/index.ts @@ -1,6 +1,6 @@ import "server-only"; -export { capturePostHogEvent, groupIdentifyPostHog } from "./capture"; +export { capturePostHogEvent, groupIdentifyPostHog, identifyPostHogPerson } from "./capture"; export type { PostHogGroupContext } from "./capture"; export { getPostHogFeatureFlag } from "./get-feature-flag"; export type { TPostHogFeatureFlagContext, TPostHogFeatureFlagValue } from "./types"; diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index 5e7fdd91851c..a28374f81a74 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Frage duplizieren", "edit_link": "Bearbeitungslink", "edit_recall": "Erinnerung bearbeiten", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Frage nicht gefunden", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Erlaube Teilnehmenden, die Sprache jederzeit zu wechseln. Mindestens 2 aktive Sprachen erforderlich.", "enable_recaptcha_to_protect_your_survey_from_spam": "Der Spam-Schutz nutzt reCAPTCHA v3, um Spam-Antworten herauszufiltern.", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 3d3025ccc7a6..c2bad846def1 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Duplicate question", "edit_link": "Edit link", "edit_recall": "Edit Recall", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Question not found", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Allow respondents to switch language at any time. Needs min. 2 active languages.", "enable_recaptcha_to_protect_your_survey_from_spam": "Spam protection uses reCAPTCHA v3 to filter out the spam responses.", diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index 5615e4a995b0..f4e96c330d5d 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Duplicar pregunta", "edit_link": "Editar enlace", "edit_recall": "Editar recuperación", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Pregunta no encontrada", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir a los participantes cambiar el idioma de la encuesta en cualquier momento durante la encuesta.", "enable_recaptcha_to_protect_your_survey_from_spam": "La protección contra spam utiliza reCAPTCHA v3 para filtrar las respuestas spam.", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index 7f8ffe41cc6f..e8e9a208fccc 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Dupliquer la question", "edit_link": "Modifier le lien", "edit_recall": "Modifier le rappel", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Question non trouvée", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permettre aux répondants de changer de langue à tout moment. Nécessite au moins 2 langues actives.", "enable_recaptcha_to_protect_your_survey_from_spam": "La protection contre le spam utilise reCAPTCHA v3 pour filtrer les réponses indésirables.", diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json index a68342f7b866..50b25c392ca8 100644 --- a/apps/web/locales/hu-HU.json +++ b/apps/web/locales/hu-HU.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Kérdés kettőzése", "edit_link": "Hivatkozás szerkesztése", "edit_recall": "Visszahívás szerkesztése", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "A kérdés nem található", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Lehetővé tétel a válaszadóknak, hogy bármikor nyelvet váltsanak. Legalább 2 aktív nyelvet igényel.", "enable_recaptcha_to_protect_your_survey_from_spam": "A szemét elleni védekezés a reCAPTCHA v3-at használja a kéretlen válaszok kiszűréséhez.", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index a2f41394e174..64f13ac99e9f 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -3074,6 +3074,11 @@ "duplicate_question": "質問を複製", "edit_link": "編集 リンク", "edit_recall": "リコールを編集", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "質問が見つかりません", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "回答者がいつでも言語を切り替えられるようにします。最低2つのアクティブな言語が必要です。", "enable_recaptcha_to_protect_your_survey_from_spam": "スパム対策はreCAPTCHA v3を使用してスパム回答をフィルタリングします。", diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index b32c0c15a0e8..963afea85e9f 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Vraag dupliceren", "edit_link": "Link bewerken", "edit_recall": "Bewerken Terugroepen", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Vraag niet gevonden", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Sta respondenten toe om op elk moment van taal te wisselen. Vereist min. 2 actieve talen.", "enable_recaptcha_to_protect_your_survey_from_spam": "Spambeveiliging maakt gebruik van reCAPTCHA v3 om de spamreacties eruit te filteren.", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 5864efe3485e..6b6204d5dcb7 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Duplicar pergunta", "edit_link": "Editar link", "edit_recall": "Editar Lembrete", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Pergunta não encontrada", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir que os respondentes alterem o idioma a qualquer momento. Necessita de no mínimo 2 idiomas ativos.", "enable_recaptcha_to_protect_your_survey_from_spam": "A proteção contra spam usa o reCAPTCHA v3 para filtrar as respostas de spam.", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index a4bab39b6009..0681b7afb99c 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Duplicar pergunta", "edit_link": "Editar link", "edit_recall": "Editar Lembrete", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Pergunta não encontrada", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir que os inquiridos mudem de idioma a qualquer momento. Necessita de pelo menos 2 idiomas ativos.", "enable_recaptcha_to_protect_your_survey_from_spam": "A proteção contra spam usa o reCAPTCHA v3 para filtrar as respostas de spam.", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index d658731db21d..e304a3c21734 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Duplică întrebarea", "edit_link": "Editare legătură", "edit_recall": "Editează Referințele", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Întrebarea nu a fost găsită", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permite respondenților să schimbe limba în orice moment. Necesită minimum 2 limbi active.", "enable_recaptcha_to_protect_your_survey_from_spam": "Protecția împotriva spamului folosește reCAPTCHA v3 pentru a filtra răspunsurile de spam.", diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index 16aa4b61f9b2..c90e1da44fd2 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Дублировать вопрос", "edit_link": "Редактировать ссылку", "edit_recall": "Редактировать напоминание", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Вопрос не найден", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Разрешить респондентам менять язык опроса в любое время. Требуется минимум 2 активных языка.", "enable_recaptcha_to_protect_your_survey_from_spam": "Для защиты от спама используется reCAPTCHA v3, чтобы отфильтровывать спам-ответы.", diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index d961e42543cb..207d64a12837 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Duplicera fråga", "edit_link": "Redigera länk", "edit_recall": "Redigera återkallning", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Fråga hittades inte", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Tillåt respondenter att byta språk när som helst. Kräver minst 2 aktiva språk.", "enable_recaptcha_to_protect_your_survey_from_spam": "Spamskydd använder reCAPTCHA v3 för att filtrera bort spam-svar.", diff --git a/apps/web/locales/tr-TR.json b/apps/web/locales/tr-TR.json index 7bead4fefd6c..18b105da4b71 100644 --- a/apps/web/locales/tr-TR.json +++ b/apps/web/locales/tr-TR.json @@ -3074,6 +3074,11 @@ "duplicate_question": "Soruyu çoğalt", "edit_link": "Bağlantıyı düzenle", "edit_recall": "Hatırlatmayı düzenle", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "Soru bulunamadı", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Katılımcıların istediği zaman anket dilini değiştirmesine izin ver. En az 2 aktif dil gerektirir.", "enable_recaptcha_to_protect_your_survey_from_spam": "Spam koruması, spam yanıtları filtrelemek için reCAPTCHA v3 kullanır.", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index ec5d5ef05f40..04bb95f82eb6 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -3074,6 +3074,11 @@ "duplicate_question": "复制问题", "edit_link": "编辑 链接", "edit_recall": "编辑 调用", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "未找到问题", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "允许受访者在调查过程中随时切换语言。需要至少启用两种语言。", "enable_recaptcha_to_protect_your_survey_from_spam": "垃圾 邮件 保护 使用 reCAPTCHA v3 来 过滤 掉 垃圾 响应 。", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 0da5cefae1a8..283eede45696 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -3074,6 +3074,11 @@ "duplicate_question": "複製問題", "edit_link": "編輯連結", "edit_recall": "編輯回憶", + "element_category_choice": "Choice", + "element_category_contact": "Contact", + "element_category_content": "Content", + "element_category_input": "Input", + "element_category_scoring": "Rating & Scoring", "element_not_found": "找不到問題", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "允許受訪者隨時切換語言。需要至少啟用兩種語言。", "enable_recaptcha_to_protect_your_survey_from_spam": "垃圾郵件保護使用 reCAPTCHA v3 過濾垃圾回應。", diff --git a/apps/web/modules/auth/signup/actions.test.ts b/apps/web/modules/auth/signup/actions.test.ts index 4b5096e3edeb..126592fc7cd7 100644 --- a/apps/web/modules/auth/signup/actions.test.ts +++ b/apps/web/modules/auth/signup/actions.test.ts @@ -37,7 +37,11 @@ vi.mock("@/modules/auth/signup/lib/team", () => ({ createTeamMembership: vi.fn() vi.mock("@/modules/auth/signup/lib/utils", () => ({ verifyTurnstileToken: vi.fn() })); vi.mock("@/lib/membership/service", () => ({ createMembership: vi.fn() })); vi.mock("@/lib/organization/service", () => ({ createOrganization: vi.fn(), getOrganization: vi.fn() })); -vi.mock("@/lib/posthog", () => ({ capturePostHogEvent: vi.fn(), groupIdentifyPostHog: vi.fn() })); +vi.mock("@/lib/posthog", () => ({ + capturePostHogEvent: vi.fn(), + groupIdentifyPostHog: vi.fn(), + identifyPostHogPerson: vi.fn(), +})); vi.mock("@/modules/ee/billing/lib/organization-billing", () => ({ ensureCloudStripeSetupForOrganization: vi.fn(), })); diff --git a/apps/web/modules/auth/signup/actions.ts b/apps/web/modules/auth/signup/actions.ts index 4dfa4ebdd5eb..310f5d3e14f0 100644 --- a/apps/web/modules/auth/signup/actions.ts +++ b/apps/web/modules/auth/signup/actions.ts @@ -13,7 +13,7 @@ import { IS_FORMBRICKS_CLOUD, IS_TURNSTILE_CONFIGURED, TURNSTILE_SECRET_KEY } fr import { verifyInviteToken } from "@/lib/jwt"; import { createMembership } from "@/lib/membership/service"; import { createOrganization, getOrganization } from "@/lib/organization/service"; -import { capturePostHogEvent, groupIdentifyPostHog } from "@/lib/posthog"; +import { capturePostHogEvent, groupIdentifyPostHog, identifyPostHogPerson } from "@/lib/posthog"; import { getUserByEmail } from "@/lib/user/service"; import { actionClient } from "@/lib/utils/action-client"; import { ActionClientCtx } from "@/lib/utils/action-client/types/context"; @@ -294,6 +294,7 @@ export const createUserAction = actionClient.inputSchema(ZCreateUserAction).acti const hasAttributionCookie = cookieStore.get(ATTRIBUTION_COOKIE_NAME) !== undefined; const attributionProperties = getAttributionPropertiesFromCookies(cookieStore); + identifyPostHogPerson(user.id, { email: user.email, name: user.name }); capturePostHogEvent( user.id, "user_signed_up", diff --git a/apps/web/modules/ee/sso/lib/better-auth-hooks.test.ts b/apps/web/modules/ee/sso/lib/better-auth-hooks.test.ts index 53af8176152c..b4e8c33fb838 100644 --- a/apps/web/modules/ee/sso/lib/better-auth-hooks.test.ts +++ b/apps/web/modules/ee/sso/lib/better-auth-hooks.test.ts @@ -2,6 +2,7 @@ import { getOAuthState } from "better-auth/api"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { prisma } from "@formbricks/database"; import { SIGNUP_EMAIL_DOMAIN_BLOCKED_ERROR_CODE } from "@formbricks/types/errors"; +import { identifyPostHogPerson } from "@/lib/posthog"; import { findMatchingLocale } from "@/lib/utils/locale"; import { isSignupEmailDomainBlocked } from "@/modules/auth/lib/signup-email-domain"; import { isSignupDomainAllowed } from "@/modules/auth/lib/signup-request-context"; @@ -37,6 +38,7 @@ vi.mock("better-auth/api", () => ({ }, })); vi.mock("@formbricks/database", () => ({ prisma: { user: { findUnique: vi.fn() } } })); +vi.mock("@/lib/posthog", () => ({ identifyPostHogPerson: vi.fn() })); vi.mock("@/lib/utils/locale", () => ({ findMatchingLocale: vi.fn() })); vi.mock("@/modules/ee/license-check/lib/utils", () => ({ getIsSsoEnabled: vi.fn(), @@ -217,6 +219,7 @@ describe("ssoDatabaseHooks.user.create.after", () => { signupSource: "direct", attributionProperties: {}, }); + expect(identifyPostHogPerson).toHaveBeenCalledWith("u1", { email: "a@b.com", name: undefined }); }); test("does nothing when no decision is stashed (e.g. non-SSO sign-up)", async () => { diff --git a/apps/web/modules/ee/sso/lib/better-auth-hooks.ts b/apps/web/modules/ee/sso/lib/better-auth-hooks.ts index 2aed22809815..732bbe60da2b 100644 --- a/apps/web/modules/ee/sso/lib/better-auth-hooks.ts +++ b/apps/web/modules/ee/sso/lib/better-auth-hooks.ts @@ -5,6 +5,7 @@ import { cookies } from "next/headers"; import { prisma } from "@formbricks/database"; import { SIGNUP_EMAIL_DOMAIN_BLOCKED_ERROR_CODE } from "@formbricks/types/errors"; import { WEBAPP_URL } from "@/lib/constants"; +import { identifyPostHogPerson } from "@/lib/posthog"; import { findMatchingLocale } from "@/lib/utils/locale"; import { getAttributionPropertiesFromCookies } from "@/modules/auth/lib/attribution"; import { isSignupEmailDomainBlocked } from "@/modules/auth/lib/signup-email-domain"; @@ -136,12 +137,18 @@ export const ssoDatabaseHooks: NonNullable = await provisionSsoUserMemberships({ userId: user.id, email: user.email, + name: user.name, provider: identityProvider, organizationId: decision.organizationId, assignToDefaultTeam: decision.assignToDefaultTeam, signupSource: decision.signupSource, attributionProperties: getSsoAttributionProperties(), }); + + // Mark the new SSO user as identified in PostHog (parity with the credentials sign-up + // path). provisionSsoUserMemberships only emits a bare `user_signed_up` capture, which + // keeps the person unidentified; this fires `$identify` so `is_identified` flips. + identifyPostHogPerson(user.id, { email: user.email, name: user.name }); }, }, }, diff --git a/apps/web/modules/ee/sso/lib/sso-provisioning.integration.test.ts b/apps/web/modules/ee/sso/lib/sso-provisioning.integration.test.ts index 2efa1de1a5f4..2fdf978985bd 100644 --- a/apps/web/modules/ee/sso/lib/sso-provisioning.integration.test.ts +++ b/apps/web/modules/ee/sso/lib/sso-provisioning.integration.test.ts @@ -18,7 +18,7 @@ vi.mock("@/modules/auth/lib/brevo", () => ({ createBrevoCustomer: vi.fn(), deleteBrevoCustomerByEmail: vi.fn(), })); -vi.mock("@/lib/posthog", () => ({ capturePostHogEvent: vi.fn() })); +vi.mock("@/lib/posthog", () => ({ capturePostHogEvent: vi.fn(), identifyPostHogPerson: vi.fn() })); // Pin multi-org OFF so the gate test exercises the fresh-instance branch specifically (the multi-org // branch returns the same shape; the license flag is an external input, not the behavior under test). vi.mock("@/modules/ee/license-check/lib/utils", async (importOriginal) => { diff --git a/apps/web/modules/ee/sso/lib/sso-provisioning.test.ts b/apps/web/modules/ee/sso/lib/sso-provisioning.test.ts index 7d9ecb522006..ecf3984fbb0a 100644 --- a/apps/web/modules/ee/sso/lib/sso-provisioning.test.ts +++ b/apps/web/modules/ee/sso/lib/sso-provisioning.test.ts @@ -4,7 +4,7 @@ import { logger } from "@formbricks/logger"; import { SIGNUP_EMAIL_DOMAIN_BLOCKED_ERROR_CODE } from "@formbricks/types/errors"; import { getIsFreshInstance } from "@/lib/instance/service"; import { createMembership } from "@/lib/membership/service"; -import { capturePostHogEvent } from "@/lib/posthog"; +import { capturePostHogEvent, identifyPostHogPerson } from "@/lib/posthog"; import { createBrevoCustomer } from "@/modules/auth/lib/brevo"; import { updateUser } from "@/modules/auth/lib/user"; import { resolveInviteMatch } from "@/modules/auth/signup/lib/invite"; @@ -23,7 +23,7 @@ vi.mock("@formbricks/database", () => ({ vi.mock("@formbricks/logger", () => ({ logger: { error: vi.fn(), warn: vi.fn(), debug: vi.fn() } })); vi.mock("@/lib/instance/service", () => ({ getIsFreshInstance: vi.fn() })); vi.mock("@/lib/membership/service", () => ({ createMembership: vi.fn() })); -vi.mock("@/lib/posthog", () => ({ capturePostHogEvent: vi.fn() })); +vi.mock("@/lib/posthog", () => ({ capturePostHogEvent: vi.fn(), identifyPostHogPerson: vi.fn() })); vi.mock("@/modules/auth/lib/brevo", () => ({ createBrevoCustomer: vi.fn() })); vi.mock("@/modules/auth/lib/user", () => ({ updateUser: vi.fn() })); vi.mock("@/modules/auth/signup/lib/invite", () => ({ resolveInviteMatch: vi.fn() })); @@ -293,6 +293,7 @@ describe("provisionSsoUserMemberships", () => { expect.anything() ); expect(createBrevoCustomer).toHaveBeenCalledWith({ id: "u1", email: "new@example.com" }); + expect(identifyPostHogPerson).toHaveBeenCalledWith("u1", { email: "new@example.com", name: undefined }); expect(capturePostHogEvent).toHaveBeenCalledWith("u1", "user_signed_up", { auth_provider: "google", email_domain: "example.com", diff --git a/apps/web/modules/ee/sso/lib/sso-provisioning.ts b/apps/web/modules/ee/sso/lib/sso-provisioning.ts index 01df641c9df4..1520e5dade67 100644 --- a/apps/web/modules/ee/sso/lib/sso-provisioning.ts +++ b/apps/web/modules/ee/sso/lib/sso-provisioning.ts @@ -7,7 +7,7 @@ import type { TUserNotificationSettings } from "@formbricks/types/user"; import { DEFAULT_TEAM_ID, SKIP_INVITE_FOR_SSO, WEBAPP_URL } from "@/lib/constants"; import { getIsFreshInstance } from "@/lib/instance/service"; import { createMembership } from "@/lib/membership/service"; -import { capturePostHogEvent } from "@/lib/posthog"; +import { capturePostHogEvent, identifyPostHogPerson } from "@/lib/posthog"; import { createBrevoCustomer } from "@/modules/auth/lib/brevo"; import { isSignupEmailDomainBlocked } from "@/modules/auth/lib/signup-email-domain"; import { updateUser } from "@/modules/auth/lib/user"; @@ -147,6 +147,7 @@ export const gateSsoProvisioning = async ({ export const provisionSsoUserMemberships = async ({ userId, email, + name, provider, organizationId, assignToDefaultTeam, @@ -155,6 +156,7 @@ export const provisionSsoUserMemberships = async ({ }: { userId: string; email: string; + name?: string | null; provider: IdentityProvider; organizationId: string | null; assignToDefaultTeam: boolean; @@ -206,6 +208,9 @@ export const provisionSsoUserMemberships = async ({ // Best-effort analytics + CRM sync, regardless of org assignment (parity with provisionNewSsoUser). createBrevoCustomer({ id: userId, email }); + // Identify the person before the sign-up capture so `user_signed_up` lands on an identified + // PostHog person (fires $identify + sets email/name) — parity with the credentials sign-up path. + identifyPostHogPerson(userId, { email, name }); capturePostHogEvent(userId, "user_signed_up", { // Spread attribution first so trusted, server-computed props always win on a name clash. ...attributionProperties, diff --git a/apps/web/modules/survey/editor/components/add-element-button.tsx b/apps/web/modules/survey/editor/components/add-element-button.tsx index a4e6e1f88c89..ffe092c74beb 100644 --- a/apps/web/modules/survey/editor/components/add-element-button.tsx +++ b/apps/web/modules/survey/editor/components/add-element-button.tsx @@ -8,7 +8,10 @@ import { useTranslation } from "react-i18next"; import { Workspace } from "@formbricks/database/prisma-browser"; import { cn } from "@/lib/cn"; import { + type TElement, + type TElementCategoryMeta, getCXElementTypes, + getElementCategories, getElementDefaults, getElementTypes, universalElementPresets, @@ -23,8 +26,63 @@ interface AddElementButtonProps { export const AddElementButton = ({ addElement, workspace, isCxMode }: AddElementButtonProps) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const [hoveredElementId, setHoveredElementId] = useState(null); const availableElementTypes = isCxMode ? getCXElementTypes(t) : getElementTypes(t); + const categories = getElementCategories(t); + + const handleAddElement = (elementType: TElement) => { + addElement({ + ...universalElementPresets, + ...getElementDefaults(elementType.id, workspace, t), + id: createId(), + type: elementType.id, + }); + setOpen(false); + }; + + // Group the available element types by category while keeping their defined order. + const elementsByCategory = new Map(); + for (const elementType of availableElementTypes) { + const group = elementsByCategory.get(elementType.category) ?? []; + group.push(elementType); + elementsByCategory.set(elementType.category, group); + } + + // Only render categories that actually contain elements (CX mode uses a subset). + const visibleCategories = categories.filter( + (category) => (elementsByCategory.get(category.id)?.length ?? 0) > 0 + ); + + const renderCategory = (category: TElementCategoryMeta) => { + const elements = elementsByCategory.get(category.id) ?? []; + return ( +
+

+ {category.label} +

+ {elements.map((elementType) => ( + + ))} +
+ ); + }; return (
-
+
@@ -47,35 +105,11 @@ export const AddElementButton = ({ addElement, workspace, isCxMode }: AddElement
- - {availableElementTypes.map((elementType) => ( - - ))} + +
+
{visibleCategories.filter((category) => category.column === 1).map(renderCategory)}
+
{visibleCategories.filter((category) => category.column === 2).map(renderCategory)}
+
); diff --git a/apps/web/modules/survey/lib/elements.tsx b/apps/web/modules/survey/lib/elements.tsx index 6e48099eef4d..4b76272a7188 100644 --- a/apps/web/modules/survey/lib/elements.tsx +++ b/apps/web/modules/survey/lib/elements.tsx @@ -26,11 +26,31 @@ import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements"; import { createI18nString } from "@/lib/i18n/utils"; import { replaceElementPresetPlaceholders } from "@/lib/utils/templates"; +export enum TElementCategory { + Input = "input", + Choice = "choice", + Scoring = "scoring", + Contact = "contact", + Content = "content", +} + +export type TElementCategoryMeta = { + id: TElementCategory; + label: string; + /** Which of the two picker columns this category renders in. */ + column: 1 | 2; + /** Tailwind classes for the icon "chip" (background tint + icon color). */ + iconClassName: string; + /** Tailwind classes for the category header label. */ + labelClassName: string; +}; + export type TElement = { id: string; label: string; description: string; icon: any; + category: TElementCategory; preset: any; }; @@ -40,6 +60,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.free_text"), description: t("templates.free_text_description"), icon: MessageSquareTextIcon, + category: TElementCategory.Input, preset: { headline: createI18nString("", []), placeholder: createI18nString(t("templates.free_text_placeholder"), []), @@ -52,6 +73,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.single_select"), description: t("templates.single_select_description"), icon: Rows3Icon, + category: TElementCategory.Choice, preset: { headline: createI18nString("", []), choices: [ @@ -72,6 +94,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.multi_select"), description: t("templates.multi_select_description"), icon: ListIcon, + category: TElementCategory.Choice, preset: { headline: createI18nString("", []), choices: [ @@ -96,6 +119,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.picture_selection"), description: t("templates.picture_selection_description"), icon: ImageIcon, + category: TElementCategory.Choice, preset: { headline: createI18nString("", []), allowMulti: true, @@ -107,6 +131,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.rating"), description: t("templates.rating_description"), icon: StarIcon, + category: TElementCategory.Scoring, preset: { headline: createI18nString("", []), scale: "star", @@ -120,6 +145,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.nps"), description: t("templates.nps_description"), icon: PresentationIcon, + category: TElementCategory.Scoring, preset: { headline: createI18nString("", []), lowerLabel: createI18nString(t("templates.nps_lower_label"), []), @@ -131,6 +157,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.csat"), description: t("templates.csat_description"), icon: SmilePlusIcon, + category: TElementCategory.Scoring, preset: { headline: createI18nString("", []), scale: "smiley", @@ -144,6 +171,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.ces"), description: t("templates.ces_description"), icon: GaugeIcon, + category: TElementCategory.Scoring, preset: { headline: createI18nString("", []), scale: "number", @@ -157,6 +185,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.ranking"), description: t("templates.ranking_description"), icon: ListOrderedIcon, + category: TElementCategory.Choice, preset: { headline: createI18nString("", []), choices: [ @@ -176,6 +205,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.matrix"), description: t("templates.matrix_description"), icon: Grid3X3Icon, + category: TElementCategory.Choice, preset: { headline: createI18nString("", []), rows: [ @@ -194,6 +224,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.statement_call_to_action"), description: t("templates.cta_description"), icon: MousePointerClickIcon, + category: TElementCategory.Content, preset: { headline: createI18nString("", []), ctaButtonLabel: createI18nString(t("templates.book_interview"), []), @@ -207,6 +238,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.consent"), description: t("templates.consent_description"), icon: CheckIcon, + category: TElementCategory.Content, preset: { headline: createI18nString("", []), label: createI18nString("", []), @@ -217,6 +249,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.file_upload"), description: t("templates.file_upload_description"), icon: ArrowUpFromLineIcon, + category: TElementCategory.Input, preset: { headline: createI18nString("", []), allowMultipleFiles: false, @@ -227,6 +260,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.date"), description: t("templates.date_description"), icon: CalendarDaysIcon, + category: TElementCategory.Input, preset: { headline: createI18nString("", []), format: "M-d-y", @@ -237,6 +271,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.schedule_a_meeting"), description: t("templates.schedule_a_meeting_description"), icon: PhoneIcon, + category: TElementCategory.Contact, preset: { headline: createI18nString("", []), calUserName: "rick/get-rick-rolled", @@ -247,6 +282,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.address"), description: t("templates.address_description"), icon: HomeIcon, + category: TElementCategory.Contact, preset: { headline: createI18nString("", []), addressLine1: { show: true, required: true, placeholder: { default: "Address Line 1" } }, @@ -262,6 +298,7 @@ export const getElementTypes = (t: TFunction): TElement[] => [ label: t("templates.contact_info"), description: t("templates.contact_info_description"), icon: ContactIcon, + category: TElementCategory.Contact, preset: { headline: createI18nString("", []), firstName: { show: true, required: true, placeholder: { default: "First Name" } }, @@ -273,6 +310,49 @@ export const getElementTypes = (t: TFunction): TElement[] => [ }, ]; +/** + * Ordered metadata for the element-type categories used to group the "Add Block" + * picker into two logical, color-coded columns. Order here defines the order the + * categories appear in (top-to-bottom within each column). + */ +export const getElementCategories = (t: TFunction): TElementCategoryMeta[] => [ + { + id: TElementCategory.Input, + label: t("workspace.surveys.edit.element_category_input"), + column: 1, + iconClassName: "bg-emerald-50 text-emerald-700", + labelClassName: "text-emerald-700", + }, + { + id: TElementCategory.Choice, + label: t("workspace.surveys.edit.element_category_choice"), + column: 1, + iconClassName: "bg-blue-50 text-blue-700", + labelClassName: "text-blue-700", + }, + { + id: TElementCategory.Scoring, + label: t("workspace.surveys.edit.element_category_scoring"), + column: 2, + iconClassName: "bg-amber-50 text-amber-700", + labelClassName: "text-amber-700", + }, + { + id: TElementCategory.Contact, + label: t("workspace.surveys.edit.element_category_contact"), + column: 2, + iconClassName: "bg-violet-50 text-violet-700", + labelClassName: "text-violet-700", + }, + { + id: TElementCategory.Content, + label: t("workspace.surveys.edit.element_category_content"), + column: 2, + iconClassName: "bg-slate-100 text-slate-600", + labelClassName: "text-slate-500", + }, +]; + export const getCXElementTypes = (t: TFunction) => getElementTypes(t).filter((elementType) => { return [ diff --git a/apps/web/playwright/survey.spec.ts b/apps/web/playwright/survey.spec.ts index 122e9b1c7b18..2e9153a62fcd 100644 --- a/apps/web/playwright/survey.spec.ts +++ b/apps/web/playwright/survey.spec.ts @@ -329,7 +329,7 @@ test.describe("Multi Language Survey Create", async () => { .filter({ hasText: /^Add BlockChoose the first question on your Block$/ }) .nth(1) .click(); - await page.getByRole("button", { name: "Multi-Select Ask respondents" }).click(); + await page.getByRole("button", { name: "Multi-Select", exact: true }).click(); await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.multiSelectQuestion.question); await page.getByPlaceholder("Option 1").fill(surveys.createAndSubmit.multiSelectQuestion.options[0]); await page.getByPlaceholder("Option 2").fill(surveys.createAndSubmit.multiSelectQuestion.options[1]); diff --git a/apps/web/playwright/utils/helper.ts b/apps/web/playwright/utils/helper.ts index 192dde002ee6..403cea2abb2e 100644 --- a/apps/web/playwright/utils/helper.ts +++ b/apps/web/playwright/utils/helper.ts @@ -515,7 +515,7 @@ export const createSurvey = async (page: Page, params: CreateSurveyParams) => { .filter({ hasText: new RegExp(`^${addBlock}$`) }) .nth(1) .click(); - await page.getByRole("button", { name: "Multi-Select Ask respondents" }).click(); + await page.getByRole("button", { name: "Multi-Select", exact: true }).click(); await fillRichTextEditor(page, "Question*", params.multiSelectQuestion.question); await page.getByRole("button", { name: "Add description", exact: true }).click(); await fillRichTextEditor(page, "Description", params.multiSelectQuestion.description); @@ -731,7 +731,7 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith .filter({ hasText: new RegExp(`^${addBlock}$`) }) .nth(1) .click(); - await page.getByRole("button", { name: "Multi-Select Ask respondents" }).click(); + await page.getByRole("button", { name: "Multi-Select", exact: true }).click(); await fillRichTextEditor(page, "Question*", params.multiSelectQuestion.question); await page.getByRole("button", { name: "Add description" }).click(); await fillRichTextEditor(page, "Description", params.multiSelectQuestion.description); diff --git a/charts/formbricks/README.md b/charts/formbricks/README.md index f3cfe98f218e..82e03b5ec797 100644 --- a/charts/formbricks/README.md +++ b/charts/formbricks/README.md @@ -95,6 +95,40 @@ The TEI service is internal-only (`ClusterIP`) and not exposed through ingress. When TEI auth is enabled, configure the shared key through `hub.embeddings.auth.apiKey` or `hub.embeddings.auth.existingSecret`; the chart manages both TEI `API_KEY` and Hub `EMBEDDING_PROVIDER_API_KEY` from that source. +Configure Hub enrichment providers through `hub.env`. API-key providers can keep their secrets in +`hub.existingSecret`. Providers that need credential files, custom CA bundles, or other pod-level +configuration can use the provider-neutral `hub.extraVolumes` and `hub.extraVolumeMounts` +settings, which apply to both Hub API and hub-worker. + +For example, a Vertex AI deployment can mount an existing Google credential JSON Secret and point +Application Default Credentials at it: + +```yaml +hub: + extraVolumes: + - name: google-cloud-credentials + secret: + secretName: formbricks-app-secrets + items: + - key: GOOGLE_APPLICATION_CREDENTIALS_JSON + path: credentials.json + + extraVolumeMounts: + - name: google-cloud-credentials + mountPath: /var/run/secrets/formbricks/google + readOnly: true + + env: + GOOGLE_APPLICATION_CREDENTIALS: /var/run/secrets/formbricks/google/credentials.json + SENTIMENT_PROVIDER: google-gemini + SENTIMENT_MODEL: gemini-2.5-flash + SENTIMENT_GOOGLE_CLOUD_PROJECT: your-google-cloud-project + SENTIMENT_GOOGLE_CLOUD_LOCATION: global +``` + +The chart renders these additions unchanged into both Hub processes. Keep credential values out of +values files and reference existing Kubernetes Secrets instead. + Autoscaling is opt-in for Hub API, Hub worker, and the embeddings runtime. If you scale the embeddings runtime above one replica while persistence is enabled, the cache PVC must support `ReadWriteMany`; otherwise set `hub.embeddings.persistence.enabled=false` or provide a compatible `existingClaim`. ## Web AI with self-hosted Qwen/vLLM @@ -336,10 +370,12 @@ JSON that can call Vertex AI. | hub.embeddings.service.type | string | `"ClusterIP"` | | | hub.env | object | `{}` | | | hub.existingSecret | string | `""` | | -| hub.image.digest | string | `"sha256:b22c5f8d1e2dd79224574f526a6714a3cc5372d18f4c047eaa9d18fe6ab75591"` | When set, takes precedence over tag (immutable pin). | +| hub.extraVolumeMounts | list | `[]` | Additional volume mounts for Hub API and worker. | +| hub.extraVolumes | list | `[]` | Additional pod volumes for Hub API and worker. | +| hub.image.digest | string | `"sha256:5d7e7c6138ff77984db54b7c91693d8f56017cefa74bc69390fc7191403208c5"` | When set, takes precedence over tag (immutable pin). | | hub.image.pullPolicy | string | `"IfNotPresent"` | | | hub.image.repository | string | `"ghcr.io/formbricks/hub"` | | -| hub.image.tag | string | `"0.6.0"` | Fallback when digest is empty. | +| hub.image.tag | string | `"0.8.1"` | Fallback when digest is empty. | | hub.migration.activeDeadlineSeconds | int | `900` | | | hub.migration.backoffLimit | int | `3` | | | hub.migration.ttlSecondsAfterFinished | int | `300` | | diff --git a/charts/formbricks/templates/hub-deployment.yaml b/charts/formbricks/templates/hub-deployment.yaml index 8663e71189a0..16724bd7ac8e 100644 --- a/charts/formbricks/templates/hub-deployment.yaml +++ b/charts/formbricks/templates/hub-deployment.yaml @@ -388,6 +388,10 @@ spec: {{- include "formbricks.envVar" (dict "name" $key "value" $value "context" $) | nindent 12 }} {{- end }} {{- end }} + {{- with .Values.hub.extraVolumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} {{- if .Values.hub.resources }} resources: {{- toYaml .Values.hub.resources | nindent 12 }} @@ -416,3 +420,7 @@ spec: port: 8080 failureThreshold: 30 periodSeconds: 10 + {{- with .Values.hub.extraVolumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/formbricks/templates/hub-worker-deployment.yaml b/charts/formbricks/templates/hub-worker-deployment.yaml index bca40ae42e9e..962d5d139360 100644 --- a/charts/formbricks/templates/hub-worker-deployment.yaml +++ b/charts/formbricks/templates/hub-worker-deployment.yaml @@ -99,8 +99,16 @@ spec: {{- end }} {{- end }} {{- end }} + {{- with .Values.hub.extraVolumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} {{- if .Values.hub.worker.resources }} resources: {{- toYaml .Values.hub.worker.resources | nindent 12 }} {{- end }} + {{- with .Values.hub.extraVolumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} diff --git a/charts/formbricks/values.yaml b/charts/formbricks/values.yaml index 2f69928aab28..66d557f20588 100644 --- a/charts/formbricks/values.yaml +++ b/charts/formbricks/values.yaml @@ -931,10 +931,10 @@ hub: # Pinned by digest for immutable, reproducible deployments. When digest is set it takes # precedence over tag, and deployment, init container, and migration job all resolve to the # same immutable image. Update on each Hub release. - # Current digest corresponds to ghcr.io/formbricks/hub:0.8.0. - digest: "sha256:619d6bc4572fced76720810c1ba1665943f0a91e1c0e20fadf2f27c35b0ada14" + # Current digest corresponds to ghcr.io/formbricks/hub:0.8.1. + digest: "sha256:5d7e7c6138ff77984db54b7c91693d8f56017cefa74bc69390fc7191403208c5" # Tag is a fallback for dev/non-prod when digest is cleared; keep aligned with the digest above. - tag: "0.8.0" + tag: "0.8.1" pullPolicy: IfNotPresent # Optional override for the secret Hub reads from. @@ -945,6 +945,12 @@ hub: # into the generated app secret and Envoy credential. existingSecret: "" + # Provider-neutral pod extensions applied to both Hub API and hub-worker. + # Use these for credential files, custom CA bundles, or other provider-specific runtime + # requirements without placing secret values in Helm values. + extraVolumes: [] + extraVolumeMounts: [] + # Optional env vars (non-secret). Use existingSecret for secret values such as DATABASE_URL and HUB_API_KEY. # Applied to both the Hub API and hub-worker (the worker merges hub.env), so providers set here also # reach the classify jobs. To enable sentiment/emotion/translation enrichment (Hub >= 0.7.1), set the