From e2c70976a4cace5cbec7ab920485fd26ede3f530 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:17:59 +0000 Subject: [PATCH 1/7] fix(surveys): stop announcing the public-API name placeholder on app surveys [ENG-2617] (#8982) --- .../environment/lib/data.test.ts | 3 +- .../[workspaceId]/environment/lib/data.ts | 3 +- apps/web/playwright/js.spec.ts | 10 +++++++ .../src/components/general/render-survey.tsx | 4 +-- packages/surveys/src/lib/survey-page.test.ts | 28 ++++++++++++++++++- packages/surveys/src/lib/survey-page.ts | 23 +++++++++++++++ packages/types/js-constants.ts | 19 +++++++++++++ 7 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 packages/types/js-constants.ts diff --git a/apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.test.ts b/apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.test.ts index e8d404748d5a..69f8bb0b6906 100644 --- a/apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.test.ts +++ b/apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.test.ts @@ -3,6 +3,7 @@ import { prisma } from "@formbricks/database"; import { Prisma } from "@formbricks/database/prisma"; import { logger } from "@formbricks/logger"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { PUBLIC_API_SURVEY_NAME_PLACEHOLDER } from "@formbricks/types/js-constants"; import { getWorkspaceStateData } from "./data"; vi.mock("server-only", () => ({})); @@ -120,7 +121,7 @@ describe("getWorkspaceStateData", () => { surveys: [ { ...mockWorkspaceData.surveys[0], - name: "[deprecated] survey name omitted from public API - will be removed soon", + name: PUBLIC_API_SURVEY_NAME_PLACEHOLDER, }, ], actionClasses: mockWorkspaceData.actionClasses, diff --git a/apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.ts b/apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.ts index 0fbb557f7c99..8234aa45759d 100644 --- a/apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.ts +++ b/apps/web/app/api/v1/client/[workspaceId]/environment/lib/data.ts @@ -9,6 +9,7 @@ import { TJsWorkspaceStateSurvey, TJsWorkspaceStateWorkspaceSetting, } from "@formbricks/types/js"; +import { PUBLIC_API_SURVEY_NAME_PLACEHOLDER } from "@formbricks/types/js-constants"; import { type TBaseFilters, buildSurveyInteractionRefreshMap } from "@formbricks/types/segment"; import { toLegacyLanguageCodes } from "@/lib/i18n/utils"; import { validateInputs } from "@/lib/utils/validate"; @@ -283,7 +284,7 @@ export const getWorkspaceStateData = async (workspaceId: string): Promise { await page.goto("http://localhost:3004"); await expect(page.locator("#formbricks-modal-container")).toHaveCount(1, { timeout: 120000 }); + + // The widget reads the survey from the public client API, which substitutes a placeholder for + // every survey name so names are not exposed over an unauthenticated endpoint. This is the only + // place the real API, the widget and the dialog are wired together, so it is the only place that + // can catch the placeholder leaking into what a screen reader announces: the dialog falls back + // to its generic name instead, and no heading carries the placeholder either. + const widget = page.locator("#formbricks-modal-container"); + await expect(widget.getByRole("dialog")).toHaveAttribute("aria-label", "Survey Dialog"); + await expect(widget.getByText(/\[deprecated] survey name omitted/)).toHaveCount(0); + await expect( page.locator("#questionCard-0").getByRole("link", { name: "Powered by Formbricks" }) ).toBeVisible(); diff --git a/packages/surveys/src/components/general/render-survey.tsx b/packages/surveys/src/components/general/render-survey.tsx index 6623f48192ea..06a6e24e2152 100644 --- a/packages/surveys/src/components/general/render-survey.tsx +++ b/packages/surveys/src/components/general/render-survey.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { SurveyContainerProps } from "@formbricks/types/formbricks-surveys"; -import { hasSurveyInstructions } from "@/lib/survey-page"; +import { getSurveyDisplayName, hasSurveyInstructions } from "@/lib/survey-page"; import { getSurveyLanguageTag, isRTLLanguage } from "@/lib/utils"; import { SurveyContainer } from "../wrappers/survey-container"; import { Survey } from "./survey"; @@ -106,7 +106,7 @@ export function RenderSurvey(props: Readonly) { onClose={close} isOpen={isOpen} dir={dir} - surveyName={props.survey.name} + surveyName={getSurveyDisplayName(props.survey.name)} hasInstructions={hasSurveyInstructions(props.survey)} lang={languageTag}> ({ id, name: id, elements: [] }); @@ -125,3 +126,28 @@ describe("hasSurveyInstructions", () => { ).toBe(true); }); }); + +describe("getSurveyDisplayName", () => { + test("keeps a real survey name", () => { + expect(getSurveyDisplayName("Product feedback")).toBe("Product feedback"); + }); + + test("drops the public client API's placeholder", () => { + // An app survey is fetched from the public client API, which substitutes this for every name. + // Rendering it would announce an internal deprecation notice as the dialog's accessible name. + expect(getSurveyDisplayName(PUBLIC_API_SURVEY_NAME_PLACEHOLDER)).toBeUndefined(); + }); + + test("drops a missing or empty name", () => { + expect(getSurveyDisplayName(undefined)).toBeUndefined(); + expect(getSurveyDisplayName("")).toBeUndefined(); + expect(getSurveyDisplayName(" ")).toBeUndefined(); + }); + + test("keeps a name that merely contains the placeholder as a substring", () => { + // Only an exact match is the API's substitution; anything else is a name someone chose. + expect(getSurveyDisplayName(`Re: ${PUBLIC_API_SURVEY_NAME_PLACEHOLDER}`)).toBe( + `Re: ${PUBLIC_API_SURVEY_NAME_PLACEHOLDER}` + ); + }); +}); diff --git a/packages/surveys/src/lib/survey-page.ts b/packages/surveys/src/lib/survey-page.ts index f44c1f66920e..8c43f2bdc2ba 100644 --- a/packages/surveys/src/lib/survey-page.ts +++ b/packages/surveys/src/lib/survey-page.ts @@ -1,4 +1,7 @@ +// Imported from the dependency-free constants module rather than from `./js`: a value import of +// `@formbricks/types/js` pulls its zod schema graph into the widget bundle (+94 kB on the UMD build). import { type TJsWorkspaceStateSurvey } from "@formbricks/types/js"; +import { PUBLIC_API_SURVEY_NAME_PLACEHOLDER } from "@formbricks/types/js-constants"; /** Id of the visually-hidden region holding the survey's persistent instructions. */ export const SURVEY_INSTRUCTIONS_ID = "fb__survey-instructions"; @@ -43,6 +46,26 @@ export const getSurveyPagePosition = ( return { index: total, total }; }; +/** + * The survey name to show a respondent, or `undefined` when there is none to show. + * + * A survey delivered by the JS widget is fetched from the public client API, which deliberately + * replaces every name with a placeholder so names are not exposed over an unauthenticated endpoint + * (ENG-808). That placeholder is an internal deprecation notice, so rendering it is worse than + * rendering nothing: it became the dialog's accessible name and its only heading on every app + * survey, which is what a screen reader then announced. Treat it as "this survey has no name" and + * let the callers fall back the way they already do for a survey rendered without one. + * + * Link surveys are unaffected — their name comes from the server component, never from this + * endpoint, so it never matches the placeholder. + * + * Also drops a name that is only whitespace, which would name the dialog with nothing at all. + */ +export const getSurveyDisplayName = (name: string | undefined): string | undefined => { + if (!name || name === PUBLIC_API_SURVEY_NAME_PLACEHOLDER) return undefined; + return name.trim().length > 0 ? name : undefined; +}; + /** * Whether the survey has instructions worth exposing on every page. * diff --git a/packages/types/js-constants.ts b/packages/types/js-constants.ts new file mode 100644 index 000000000000..92e950fcd939 --- /dev/null +++ b/packages/types/js-constants.ts @@ -0,0 +1,19 @@ +/** + * Constants shared between the public client API and the JS widget that consumes it. + * + * This module deliberately imports nothing. Its consumers include `@formbricks/surveys`, whose + * bundle every respondent of an app survey downloads, and importing a *value* from `./js` instead + * would drag that module's zod schema graph into the bundle — measured at +94 kB on the UMD build. + * Keep it dependency-free, and keep constants the widget needs here rather than in `./js`. + */ + +/** + * The string the public client API substitutes for every survey name it returns, so that survey + * names are not exposed over an unauthenticated endpoint (ENG-808). + * + * It is a marker, not text anyone should ever read: it lives here rather than inline at the + * substitution site so the widget can recognise it and refuse to render it. Change it in one place + * or the widget stops recognising it — see `getSurveyDisplayName` in `@formbricks/surveys`. + */ +export const PUBLIC_API_SURVEY_NAME_PLACEHOLDER = + "[deprecated] survey name omitted from public API - will be removed soon"; From 10591b84fae1e1b64e1721b6da1d6a53c93915b6 Mon Sep 17 00:00:00 2001 From: Johannes <72809645+jobenjada@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:24:30 +0000 Subject: [PATCH 2/7] fix: correct vertical misalignment of type select in survey variables row (#8963) --- .../survey/editor/components/survey-variables-card-item.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx b/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx index 4ed95d9e4cc7..bbdf9c411072 100644 --- a/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx +++ b/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx @@ -202,6 +202,7 @@ export const SurveyVariablesCardItem = ({ name="type" render={({ field }) => ( form.handleSubmit(editSurveyVariable)() : undefined}>