diff --git a/apps/web/app/api/auth/[...all]/route.ts b/apps/web/app/api/auth/[...all]/route.ts index 548693302ee6..17e24ec9515e 100644 --- a/apps/web/app/api/auth/[...all]/route.ts +++ b/apps/web/app/api/auth/[...all]/route.ts @@ -1,4 +1,8 @@ import { auth } from "@/modules/auth/lib/auth"; +import { + recordSsoCallbackOutcome, + recordSsoCallbackThrow, +} from "@/modules/auth/lib/better-auth-observability"; import { createAuthPathLabeller } from "@/modules/auth/lib/better-auth-path-label"; import { runWithBetterAuthRequestContext } from "@/modules/auth/lib/better-auth-request-context"; import { runWithEmailVerificationRequestContext } from "@/modules/auth/lib/email-verification-request-context"; @@ -62,17 +66,31 @@ const handler = async (request: Request): Promise => { // neither is ours to change: the pinned SSO callback path, and `application_type` on dynamic client // registration (see each module). Both no-op for every other request. const mappedRequest = await normalizeDcrRequest(mapLegacySsoCallbackRequest(request)); - return runWithBetterAuthRequestContext( - { path: labelAuthPath(mappedRequest.url), method: mappedRequest.method }, - () => - runWithSsoRequestContext(() => - // ENG-2562: carries "this request just verified an email" from Better Auth's - // `afterEmailVerification` hook to the `hooks.after` chain, which is where the session can - // actually be minted. Innermost because it is the narrowest scope of the three — one endpoint, - // not the whole handler. - runWithEmailVerificationRequestContext(() => auth.handler(mappedRequest)) - ) - ); + try { + const response = await runWithBetterAuthRequestContext( + { path: labelAuthPath(mappedRequest.url), method: mappedRequest.method }, + () => + runWithSsoRequestContext(() => + // ENG-2562: carries "this request just verified an email" from Better Auth's + // `afterEmailVerification` hook to the `hooks.after` chain, which is where the session can + // actually be minted. Innermost because it is the narrowest scope of the three — one endpoint, + // not the whole handler. + runWithEmailVerificationRequestContext(() => auth.handler(mappedRequest)) + ) + ); + // ENG-2551: the one place that sees the outcome of every SSO callback, whatever went wrong and + // whichever provider it was — a failed callback is a redirect carrying `?error=`, or a 4xx/5xx. + // Emitted here rather than from a hook because the failures that matter most are the ones Better + // Auth returns as a response rather than throwing, so no error-path hook observes them. + recordSsoCallbackOutcome(mappedRequest.url, response); + return response; + } catch (error) { + // A throw is the most severe callback failure there is — Next answers 500 and the user cannot sign + // in — so it must not be the one case the signal misses. Recorded, then rethrown unchanged so the + // existing error handling (and the Sentry capture in this module) behaves exactly as before. + recordSsoCallbackThrow(mappedRequest.url); + throw error; + } }; export { handler as GET, handler as POST }; diff --git a/apps/web/modules/auth/lib/better-auth-observability.test.ts b/apps/web/modules/auth/lib/better-auth-observability.test.ts index db2ca45c81cf..dd251f3d81f6 100644 --- a/apps/web/modules/auth/lib/better-auth-observability.test.ts +++ b/apps/web/modules/auth/lib/better-auth-observability.test.ts @@ -14,6 +14,8 @@ import { auditVerificationSessionWithheld, betterAuthLogger, getSignInAuthMethod, + recordSsoCallbackOutcome, + recordSsoCallbackThrow, redactEmailsInLogMessage, signInAuditDatabaseHook, } from "./better-auth-observability"; @@ -523,6 +525,9 @@ describe("betterAuthLogger — OAuth state errors (ENG-2471)", () => { log("error", "State mismatch: verification not found", stateError); + // Anchor the negative: an empty `mock.calls` stringifies to "[]", which contains no secret, so + // without this the assertion would hold even if nothing were ever logged. + expect(logger.withContext).toHaveBeenCalled(); expect(JSON.stringify(vi.mocked(logger.withContext).mock.calls)).not.toContain( "super-secret-state-value" ); @@ -704,3 +709,275 @@ describe("betterAuthLogger (request-path tagging, ENG-2259)", () => { expect(Sentry.captureException).not.toHaveBeenCalled(); }); }); + +/** + * ENG-2551: the SSO callback outcome signal. Its whole purpose is to be alertable, so the tests are + * about the *field values* an alert would key on, not about the human-readable message. + * + * The motivating incident is ENG-2750, where 100% of Cloud Microsoft sign-ins failed for ~18 hours + * and produced no Sentry event at all — Better Auth logs that failure as a bare message with no + * `Error`, so the capture gate above never sees anything. Hence a signal keyed on the outcome. + */ +describe("recordSsoCallbackOutcome (ENG-2551)", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const redirect = (location: string, status = 302) => new Response(null, { status, headers: { location } }); + + /** + * A completed sign-in, which Better Auth marks by minting the session cookie. Success is asserted + * through that rather than through the redirect target: `getSsoReturnToUrl` preserves the caller's + * query string, so a legitimate `callbackURL` can carry `?error=…` of its own. + */ + const signedInRedirect = (location: string) => { + const response = new Response(null, { status: 302, headers: { location } }); + response.headers.append("set-cookie", "__Secure-formbricks.session_token=abc; Path=/; HttpOnly"); + return response; + }; + + const contextOf = () => vi.mocked(logger.withContext).mock.calls[0]?.[0]; + + test.each([ + ["the Better Auth path", "https://app.test/api/auth/callback/azuread"], + ["the pinned legacy path", "https://app.test/api/auth/oauth2/callback/azuread"], + ["a trailing slash", "https://app.test/api/auth/callback/azuread/"], + ])("records a failed callback on %s", (_label, url) => { + recordSsoCallbackOutcome(url, redirect("https://app.test/auth/login?error=unable_to_get_user_info")); + + expect(contextOf()).toEqual({ + source: "sso-callback", + ssoCallbackOutcome: "failure", + ssoProvider: "azuread", + ssoCallbackReason: "unable_to_get_user_info", + }); + expect(contextLoggerMock.warn).toHaveBeenCalledWith("SSO callback failed"); + }); + + test("records a successful callback, so the alert can key on a ratio", () => { + recordSsoCallbackOutcome("https://app.test/api/auth/callback/openid", signedInRedirect("/")); + + expect(contextOf()).toEqual({ + source: "sso-callback", + ssoCallbackOutcome: "success", + ssoProvider: "openid", + }); + expect(contextLoggerMock.info).toHaveBeenCalledWith("SSO callback succeeded"); + expect(contextLoggerMock.warn).not.toHaveBeenCalled(); + }); + + /** + * The two failure shapes that do not redirect, and the reason this reads the response rather than + * hooking an error path: the SSO licence gate answers 403 and an unhandled fault answers 500. + * Neither throws where an error hook would see it. + */ + test.each([ + [403, "http_403"], + [500, "http_500"], + ])("treats a %i on the callback as a failure", (status, reason) => { + recordSsoCallbackOutcome("https://app.test/api/auth/callback/saml", new Response(null, { status })); + + expect(contextOf()).toMatchObject({ ssoCallbackOutcome: "failure", ssoCallbackReason: reason }); + }); + + // Cardinality guard: anyone can request `/api/auth/callback/`, and a log field an + // outsider can fill with unbounded distinct values is a log field no alert can group on. + test.each([ + ["an unregistered provider id", "https://app.test/api/auth/callback/not-a-provider"], + ["a very long segment", `https://app.test/api/auth/callback/${"a".repeat(300)}`], + ])("buckets %s to unknown", (_label, url) => { + recordSsoCallbackOutcome(url, redirect("/")); + + expect(contextOf()).toMatchObject({ ssoProvider: "unknown" }); + }); + + /** + * The reason needs the same protection as the provider, and the reason is easy to miss: Better Auth + * **echoes the inbound `error` query parameter** into its redirect (`callback.mjs`, + * `if (error) redirectOnError(error, error_description)`). Anyone can get a parseable `state` by + * starting a sign-in, then call the callback with `&error=` — so without an allow-list an + * outsider both inflates this field's cardinality and can forge a specific reason to disguise a + * real outage. A charset regex would not help: it bounds the shape, not the number of values. + */ + test.each([ + ["a plausible but unknown code", "totally_made_up_code"], + ["a forged real-looking code", "state_mismatch_2"], + ["markup", "%3Cscript%3E+injected"], + ])("buckets %s to other", (_label, injected) => { + recordSsoCallbackOutcome( + "https://app.test/api/auth/callback/google", + redirect(`https://app.test/auth/login?error=${injected}`) + ); + + expect(contextOf()).toMatchObject({ ssoCallbackReason: "other" }); + }); + + test.each([ + ["Better Auth's own callback code", "unable_to_get_user_info"], + ["a StateError code", "state_mismatch"], + ["a code this app emits", "account_not_linked"], + // What a database outage mid-callback actually produces — verified by stopping Postgres against a + // live instance. Bucketing this one would hide the clearest infrastructure signal there is. + ["the code an infrastructure fault produces", "internal_server_error"], + ])("records %s verbatim", (_label, code) => { + recordSsoCallbackOutcome( + "https://app.test/api/auth/callback/azuread", + redirect(`https://app.test/auth/login?error=${code}`) + ); + + expect(contextOf()).toMatchObject({ ssoCallbackReason: code }); + }); + + /** + * The worst callback failure is the one that never produces a response at all: the request throws, + * Next answers 500, nobody signs in. Recording only responses would have left precisely that case + * invisible to the alert. + */ + test("records a callback that threw", () => { + recordSsoCallbackThrow("https://app.test/api/auth/callback/azuread"); + + expect(contextOf()).toEqual({ + source: "sso-callback", + ssoCallbackOutcome: "failure", + ssoProvider: "azuread", + ssoCallbackReason: "exception", + }); + expect(contextLoggerMock.warn).toHaveBeenCalledWith("SSO callback failed"); + }); + + test("stays silent when a non-callback endpoint throws", () => { + recordSsoCallbackThrow("https://app.test/api/auth/sign-in/email"); + + expect(logger.withContext).not.toHaveBeenCalled(); + }); + + test.each([ + ["a non-callback auth endpoint", "https://app.test/api/auth/sign-in/email"], + ["the callback list root", "https://app.test/api/auth/callback"], + ["an unparseable URL", "not-a-url"], + ])("stays silent for %s", (_label, url) => { + recordSsoCallbackOutcome(url, redirect("/")); + + expect(logger.withContext).not.toHaveBeenCalled(); + }); + + /** + * A redirect the browser cannot follow is a failed sign-in. Both of these previously corrupted the + * ratio rather than merely losing detail — a missing `Location` was recorded as a *success*, and a + * malformed one threw into the outer catch so the callback vanished from both sides of the ratio. + * The earlier test here asserted only that it did not throw, which is the weaker property and is + * why the suite stayed green. + */ + /** + * `URLSearchParams.get` reads both `?error=` and a bare `?error` back as `""`, so a truthiness + * check counted an ambiguous error parameter as a clean sign-in. Only a genuinely absent parameter + * (`null`) is a success — same reasoning as the two cases below: anything the browser cannot + * usefully follow belongs on the failure side, because success is the half the ratio must trust. + */ + test.each([ + ["an empty error value", "https://app.test/auth/login?error="], + ["a valueless error parameter", "https://app.test/auth/login?error"], + ])("records %s as a failure, not a success", (_label, location) => { + recordSsoCallbackOutcome("https://app.test/api/auth/callback/azuread", redirect(location)); + + expect(contextOf()).toMatchObject({ ssoCallbackOutcome: "failure", ssoCallbackReason: "other" }); + }); + + test("still records a redirect with no error parameter as a success", () => { + recordSsoCallbackOutcome( + "https://app.test/api/auth/callback/azuread", + signedInRedirect("https://app.test/?welcome=1") + ); + + expect(contextOf()).toMatchObject({ ssoCallbackOutcome: "success" }); + }); + + /** + * #9026 review, P1. With `response_mode=form_post` Better Auth turns the POST callback into a 302 to + * its own GET twin *before* validating state or exchanging the code, and `legacy-sso-callback.ts` + * preserves that flow deliberately. Recording the hop adds a second outcome per sign-in, and since + * the hop carries no `error` it lands on the success side — so a form_post flow failing 100% of the + * time would still read as only 50% failures, under any threshold the alert picks. + */ + test.each([ + ["the Better Auth twin", "https://app.test/api/auth/callback/azuread?code=abc&state=xyz"], + ["a relative twin", "/api/auth/callback/azuread?code=abc&state=xyz"], + ])("says nothing for the form_post hop to %s", (_label, location) => { + recordSsoCallbackOutcome("https://app.test/api/auth/callback/azuread", redirect(location)); + + expect(logger.withContext).not.toHaveBeenCalled(); + }); + + test("the GET that follows the hop still records the real outcome", () => { + recordSsoCallbackOutcome( + "https://app.test/api/auth/callback/azuread", + redirect("https://app.test/auth/login?error=state_mismatch") + ); + + expect(contextOf()).toMatchObject({ + ssoCallbackOutcome: "failure", + ssoCallbackReason: "state_mismatch", + }); + }); + + /** + * #9026 review, P2. The success destination is the caller's `callbackURL`, and `getSsoReturnToUrl` + * keeps its query string — so `/page?error=retry` is a supported target. Reading `error` off the + * redirect cannot tell that apart from Better Auth's own error redirect, which goes to the caller's + * `errorCallbackURL` (`/auth/login` for every button here). The session cookie can. + */ + test("a completed sign-in whose callbackURL carries its own error parameter is a success", () => { + recordSsoCallbackOutcome( + "https://app.test/api/auth/callback/azuread", + signedInRedirect("https://app.test/some-page?error=retry") + ); + + expect(contextOf()).toMatchObject({ ssoCallbackOutcome: "success" }); + expect(contextLoggerMock.warn).not.toHaveBeenCalled(); + }); + + test("a failure redirect to errorCallbackURL is still a failure", () => { + recordSsoCallbackOutcome( + "https://app.test/api/auth/callback/azuread", + redirect("https://app.test/auth/login?error=account_not_linked") + ); + + expect(contextOf()).toMatchObject({ + ssoCallbackOutcome: "failure", + ssoCallbackReason: "account_not_linked", + }); + }); + + /** + * Verify-before-link recovery ends here on purpose: no session, no error, an inbox-verification + * page. Counting it either way corrupts the ratio, so it is named and excluded from both sides. + */ + test("recovery, with no session and no error, is neither a success nor a failure", () => { + recordSsoCallbackOutcome( + "https://app.test/api/auth/callback/openid", + redirect("https://app.test/auth/verification-requested") + ); + + expect(contextOf()).toMatchObject({ + ssoCallbackOutcome: "incomplete", + ssoCallbackReason: "no_session", + }); + expect(contextLoggerMock.warn).not.toHaveBeenCalled(); + expect(contextLoggerMock.info).toHaveBeenCalledWith("SSO callback did not complete a sign-in"); + }); + + test.each([ + ["a redirect with no Location", undefined, "missing_location"], + ["a malformed Location", "http://[", "malformed_location"], + ])("records %s as a failure", (_label, location, reason) => { + expect(() => + recordSsoCallbackOutcome( + "https://app.test/api/auth/callback/azuread", + new Response(null, { status: 302, ...(location ? { headers: { location } } : {}) }) + ) + ).not.toThrow(); + + expect(contextOf()).toMatchObject({ ssoCallbackOutcome: "failure", ssoCallbackReason: reason }); + expect(contextLoggerMock.warn).toHaveBeenCalledWith("SSO callback failed"); + }); +}); diff --git a/apps/web/modules/auth/lib/better-auth-observability.ts b/apps/web/modules/auth/lib/better-auth-observability.ts index bf76ded4e1ed..7c91096bb7bc 100644 --- a/apps/web/modules/auth/lib/better-auth-observability.ts +++ b/apps/web/modules/auth/lib/better-auth-observability.ts @@ -408,3 +408,243 @@ export const auditPasswordReset = async (userId: string): Promise => { logger.withContext({ source: "better-auth" }).error("Failed to queue password-reset audit event"); } }; + +/** + * Providers whose id may appear in an SSO callback outcome record (ENG-2551). + * + * An allow-list rather than a sanitising regex, because the provider id comes from the request path + * and anyone can call `/api/auth/callback/`. A regex would bound the *shape* of the value + * but not the number of distinct values, so it would hand a caller unbounded control over a log + * field's cardinality — the thing that makes a log-based alert unusable. Anything unrecognised + * buckets to `unknown`. + * + * Deliberately not imported from the SSO provider config: this module must not depend on the EE + * surface, and the list answers a different question anyway ("what may we label") rather than "what + * is registered on this instance". + */ +const SSO_CALLBACK_PROVIDER_IDS = new Set(["google", "github", "azuread", "openid", "saml"]); + +/** + * A callback path, capturing the provider id. + * + * Answers two questions: which provider a request is for, and — applied to a *redirect target* — + * whether Better Auth is bouncing a `form_post` callback to its GET twin rather than concluding. + */ +const SSO_CALLBACK_PATH = /\/callback\/([^/?#]+)\/?$/; + +/** + * `success` and `failure` are the two an alert divides; `incomplete` is deliberately neither. + * + * A ratio of `failure / (success + failure)` stays meaningful only while both sides really are + * completed sign-ins or broken ones. Verify-before-link recovery is neither — it ends on the "check + * your inbox" page by design — so it gets its own value rather than being forced into one, and an + * alert should exclude it explicitly. + */ +type SsoCallbackRecord = { outcome: "success" | "failure" | "incomplete"; reason?: string }; + +/** + * Better Auth mints the session cookie only on a completed sign-in, which is why this is the success + * signal rather than the redirect target: the success destination is the caller's `callbackURL` and + * the failure destination its `errorCallbackURL`, and both are ordinary app pages here. + */ +const hasSessionCookie = (response: Response): boolean => + response.headers.getSetCookie().some((cookie) => cookie.includes("session_token")); + +/** + * Callback failure reasons that may be recorded verbatim (ENG-2551). + * + * This has to be an allow-list for the same reason the provider id does, and the reason is easy to + * miss: Better Auth **echoes the inbound `error` query parameter** into its own redirect + * (`api/routes/callback.mjs`, `if (error) redirectOnError(error, error_description)`). Any caller can + * obtain a parseable `state` by starting a sign-in and then request + * `/api/auth/callback/?state=…&error=`, so the value that lands here is outside + * attacker control only if we bound it to a known set. A charset regex would bound the *shape* of the + * value but not the *number of distinct values*, which is the property an alert needs. + * + * Two consequences of that echo, both closed by bucketing to `other`: an outsider cannot inflate the + * cardinality of this field, and cannot forge a specific reason to make a real outage look like + * something else. + * + * Read from `better-auth/dist/oauth2/errors.mjs` (OAUTH_CALLBACK_ERROR_CODES), the two route-level + * codes in `api/routes/callback.mjs`, the five `StateError` codes in `state.mjs`, and the codes this + * app redirects with itself. A code that disappears upstream simply stops occurring; a new one + * buckets to `other` until it is added, which is the safe direction. + */ +const SSO_CALLBACK_REASONS = new Set([ + // Better Auth OAUTH_CALLBACK_ERROR_CODES + "account_already_linked_to_different_user", + "email_does_not_match", + "email_not_found", + "email_not_verified", + "invalid_code", + "issuer_mismatch", + "issuer_missing", + "no_callback_url", + "no_code", + "nonce_binding_missing", + "oauth_provider_not_found", + "unable_to_get_user_info", + "unable_to_link_account", + // Better Auth callback route, and the codes its redirectOnError call sites pass. `internal_server_error` + // is the important one and was found by smoke-testing rather than by reading the enum: stopping the + // database mid-callback produces it, so it is the code an infrastructure outage actually arrives as. + "invalid_callback_request", + "internal_server_error", + "invalid_payload", + "invalid_profile", + "missing_profile", + "payload_expired", + "user_creation_failed", + // Better Auth StateError codes + "state_generation_error", + "state_invalid", + "state_mismatch", + "state_not_found", + "state_security_mismatch", + // Emitted by this app + "OAuthAccountNotLinked", + "account_not_linked", + "invalid_scope", + "unable_to_create_user", +]); + +/** + * Record the outcome of an SSO callback, so a total sign-in outage announces itself (ENG-2551). + * + * The problem this solves is that every SSO outage so far has had a *novel cause* and an *identical + * symptom*. ENG-1800 (RFC 9207 `iss`), ENG-2555 (wrong `Account.issuer`), ENG-2750 (placeholder + * issuer) and the suppressed `state_mismatch` class each needed their own diagnosis, and each ended + * with the callback redirecting to `?error=…` instead of to the callbackURL. Alerting on causes means + * adding a rule after every incident; alerting on the outcome covers the next one for free. + * + * ENG-2750 is why this exists at all: it failed 100% of Cloud Microsoft sign-ins for ~18 hours and + * produced **no Sentry event**, because Better Auth logs that failure as a bare message with no + * `Error` argument, so the capture gate above never has anything to capture. A log line keyed on a + * stable field is what an alert can actually watch. + * + * Emits on success too: the useful alert threshold is a *ratio* ("failures exceed N% of callbacks + * over 15 minutes"), which survives traffic swings, campaigns and quiet weekends in a way an + * absolute count does not. + * + * Failures log at `warn`, not `error`. A single failed callback is routinely the user's own doing — a + * stale tab, an expired state, a declined consent — and promoting each one to `error` would degrade + * the signal this is meant to create. The alert keys on `ssoCallbackOutcome`, so the level is + * presentation, not meaning. + * + * Never throws: observability must not be able to fail a sign-in that otherwise succeeded. + */ +/** A response that minted no session: either a named failure, or a flow that never claimed to sign in. */ +const sessionlessOutcome = (target: URL): SsoCallbackRecord => { + const error = target.searchParams.get("error"); + + // `?error=` and a bare `?error` both read back as `""`. An error parameter that is present but + // empty says something went wrong without saying what, so it belongs on the failure side. + if (error !== null) { + return { outcome: "failure", reason: SSO_CALLBACK_REASONS.has(error) ? error : "other" }; + } + + // No session and no error is neither: verify-before-link recovery ends exactly here, redirecting to + // the "check your inbox" page on purpose (`ssoRecoveryAfterHandler`). Counting it as a failure would + // inflate the ratio during a perfectly healthy flow, and as a success would claim a sign-in that did + // not happen — so it is named and left out of both sides. + return { outcome: "incomplete", reason: "no_session" }; +}; + +/** The outcome carried by a redirect, or `null` when this redirect decides nothing. */ +const redirectOutcome = (target: URL, response: Response): SsoCallbackRecord | null => { + // `response_mode=form_post`: Better Auth 1.7 turns a POST callback into a 302 to the GET callback + // *before* it validates state or exchanges the code (`api/routes/callback.mjs`), and + // `legacy-sso-callback.ts` deliberately preserves that flow. Recording the hop would score every + // form_post sign-in as one extra outcome, and — because the hop carries no `error` — it would score + // it as a success, capping a totally broken form_post flow at a 50% failure ratio. The GET that + // follows is the one that knows what happened, so say nothing here. + if (SSO_CALLBACK_PATH.test(target.pathname)) return null; + + // A completed sign-in is the one thing that mints a session, and Better Auth decides that rather + // than it being inferred from where the browser is sent next. That matters because the success + // destination is the caller's `callbackURL` — `getSsoReturnToUrl` preserves its query string, so a + // legitimate target like `/page?error=retry` exists — while the failure destination is the caller's + // `errorCallbackURL` (`/auth/login` for every button here). Reading the `error` key off whichever + // URL came back cannot tell those apart; the session cookie can. + return hasSessionCookie(response) ? { outcome: "success" } : sessionlessOutcome(target); +}; + +export const recordSsoCallbackOutcome = (requestUrl: string, response: Response): void => { + emitSsoCallbackRecord(requestUrl, (): SsoCallbackRecord | null => { + // A 4xx/5xx on the callback is a failed sign-in too — this is how the SSO licence gate's 403 and + // any unhandled 500 present, and neither redirects. + if (response.status >= 400) return { outcome: "failure", reason: `http_${response.status}` }; + + if (response.status < 300) { + return hasSessionCookie(response) + ? { outcome: "success" } + : { outcome: "incomplete", reason: "no_session" }; + } + + // A redirect the browser cannot follow is a failed sign-in, not a quiet success. Both shapes below + // would otherwise corrupt the ratio rather than merely lose detail: a missing `Location` counted as + // success inflates the healthy side, and a malformed one threw into the outer catch, dropping the + // callback from both sides. + const location = response.headers.get("location"); + if (!location) return { outcome: "failure", reason: "missing_location" }; + + // The catch guards the parse and nothing else: wrapping the classification too would relabel any + // fault inside it as a malformed `Location`, which is a different and misleading claim. + let target: URL; + try { + target = new URL(location, requestUrl); + } catch { + return { outcome: "failure", reason: "malformed_location" }; + } + + return redirectOutcome(target, response); + }); +}; + +/** + * Record an SSO callback that threw rather than returning a response (ENG-2551). + * + * The most severe callback failure there is: the request never produces a redirect, Next answers 500, + * and the user simply cannot sign in. Recording only responses would have left exactly that case + * invisible to the alert this signal exists to feed. + */ +export const recordSsoCallbackThrow = (requestUrl: string): void => { + emitSsoCallbackRecord(requestUrl, () => ({ outcome: "failure", reason: "exception" })); +}; + +/** + * Shared emitter: resolves the provider from the path, applies the outcome, logs once. Never throws — + * observability must not be able to fail a sign-in that otherwise succeeded, nor mask the original + * error on the throwing path. + */ +const emitSsoCallbackRecord = (requestUrl: string, resolveOutcome: () => SsoCallbackRecord | null): void => { + try { + const path = new URL(requestUrl).pathname; + // The internal path Better Auth actually serves. `mapLegacySsoCallbackRequest` has already + // rewritten the pinned `/oauth2/callback/:id` form by the time a response exists, so matching the + // internal shape covers both. + const providerSegment = SSO_CALLBACK_PATH.exec(path)?.[1]; + if (!providerSegment) return; + + const provider = SSO_CALLBACK_PROVIDER_IDS.has(providerSegment.toLowerCase()) + ? providerSegment.toLowerCase() + : "unknown"; + // `null` means "this request is not the one that decides the outcome" — see the form_post hop. + const record = resolveOutcome(); + if (!record) return; + const { outcome, reason } = record; + + const contextLogger = logger.withContext({ + source: "sso-callback", + ssoCallbackOutcome: outcome, + ssoProvider: provider, + ...(reason ? { ssoCallbackReason: reason } : {}), + }); + + if (outcome === "failure") contextLogger.warn("SSO callback failed"); + else if (outcome === "incomplete") contextLogger.info("SSO callback did not complete a sign-in"); + else contextLogger.info("SSO callback succeeded"); + } catch { + // A malformed URL is not worth failing or logging a sign-in over. + } +}; diff --git a/apps/web/modules/ee/sso/lib/better-auth-providers.test.ts b/apps/web/modules/ee/sso/lib/better-auth-providers.test.ts index 109889ed0280..e63cb70fb280 100644 --- a/apps/web/modules/ee/sso/lib/better-auth-providers.test.ts +++ b/apps/web/modules/ee/sso/lib/better-auth-providers.test.ts @@ -499,6 +499,80 @@ describe("better-auth SSO providers", () => { }); }); + /** + * ENG-2754: the generic OIDC provider shares ENG-2750's failure mode, because it too resolves its + * endpoints from a discovery document. Pointed at a Microsoft multi-tenant authority it would + * verify id_tokens against a `{tenantid}` placeholder and fail every sign-in, so those authorities + * skip discovery here too — and the operator is told to prefer the dedicated azuread provider. + */ + const oidcBase = { + ENTERPRISE_LICENSE_KEY: "lic", + OIDC_OAUTH_ENABLED: true, + OIDC_CLIENT_ID: "oidc-id", + OIDC_CLIENT_SECRET: "oidc-secret", + }; + + test.each([ + ["https://login.microsoftonline.com/common/v2.0", "common"], + ["https://login.microsoftonline.com/organizations/v2.0", "organizations"], + // Case and a trailing slash are operator typing, not a different provider. + ["https://LOGIN.microsoftonline.com/Common/", "common"], + // No /v2.0 suffix: the authority is the first path segment either way. + ["https://login.microsoftonline.com/common", "common"], + ])("OIDC pointed at %s skips discovery and uses Microsoft's endpoints", async (issuer, authority) => { + const m = await loadProviders({ ...oidcBase, OIDC_ISSUER: issuer }); + const oidc = m.ssoGenericOAuthConfig.find((c) => c.providerId === "openid"); + + expect(oidc?.discoveryUrl).toBeUndefined(); + expect(oidc?.authorizationUrl).toBe( + `https://login.microsoftonline.com/${authority}/oauth2/v2.0/authorize` + ); + expect(oidc?.tokenUrl).toBe(`https://login.microsoftonline.com/${authority}/oauth2/v2.0/token`); + expect(oidc?.userInfoUrl).toBe("https://graph.microsoft.com/oidc/userinfo"); + // Account keying must not move, or existing linked accounts stop matching. + expect(oidc?.accountIssuer).toBe("local:oauth:openid"); + expect(loggerWarn).toHaveBeenCalledTimes(1); + // Recommends the dedicated provider, and — like the azuread warning — does not imply the + // configured authority has been discarded, since `organizations` keeps restricting sign-in. + const message = loggerWarn.mock.calls[0][0] as string; + expect(message).toContain("AZUREAD_CLIENT_ID"); + expect(message).toContain("still applies"); + expect(message).not.toMatch(/like unset|treated as unset/i); + }); + + /** + * Everything else keeps discovery. `consumers` is included deliberately: it is a Microsoft + * authority but advertises a real issuer, so it must not be swept up with the other two — the same + * distinction ENG-2750 turns on. The lookalike host guards against matching on a substring. + */ + test.each([ + ["a normal IdP", "https://idp.test"], + ["Microsoft's personal-accounts authority", "https://login.microsoftonline.com/consumers/v2.0"], + [ + "a concrete Microsoft tenant", + "https://login.microsoftonline.com/00000000-1111-2222-3333-444444444444/v2.0", + ], + ["a lookalike host", "https://login.microsoftonline.com.evil.test/common/v2.0"], + ])("OIDC keeps discovery for %s", async (_label, issuer) => { + const m = await loadProviders({ ...oidcBase, OIDC_ISSUER: issuer }); + const oidc = m.ssoGenericOAuthConfig.find((c) => c.providerId === "openid"); + + expect(oidc?.discoveryUrl).toBe(`${issuer}/.well-known/openid-configuration`); + expect(oidc?.authorizationUrl).toBeUndefined(); + expect(loggerWarn).not.toHaveBeenCalled(); + }); + + test("OIDC does not warn about a Microsoft authority when the instance is unlicensed", async () => { + const m = await loadProviders({ + ...oidcBase, + ENTERPRISE_LICENSE_KEY: undefined, + OIDC_ISSUER: "https://login.microsoftonline.com/common/v2.0", + }); + + expect(m.ssoGenericOAuthConfig.find((c) => c.providerId === "openid")).toBeUndefined(); + expect(loggerWarn).not.toHaveBeenCalled(); + }); + test("SAML bridges to the local Jackson endpoints and resolves first/last name", async () => { const m = await loadProviders({ ENTERPRISE_LICENSE_KEY: "lic", @@ -660,3 +734,109 @@ describe("Azure identity comes from Graph, not an unverified id_token (#9017 rev expect(azure?.discoveryUrl).toBeDefined(); }); }); + +/** + * Raised in review on #9023: the generic OIDC provider reaches the same explicit-endpoint branch when + * `OIDC_ISSUER` points at a Microsoft multi-tenant authority, so it inherited the same unverified + * id_token shortcut. Driven through the initialised provider, for the same reason as the azuread + * suite above: the config object cannot show which of the two paths actually runs. + * + * Also pins the sweep result — `saml` reaches an explicit-endpoint branch too but is NOT affected, + * because it requests no `openid` scope, so BoxyHQ never mints an id_token for the shortcut to read. + */ +describe("OIDC identity comes from Graph when pointed at Microsoft (#9023 review)", () => { + const forgedIdToken = `${Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url")}.${Buffer.from( + JSON.stringify({ sub: "forged-subject", email: "attacker@evil.test" }) + ).toString("base64url")}.`; + + const initializedProvider = async (providerId: string, overrides: Partial) => { + const m = await loadProviders({ ENTERPRISE_LICENSE_KEY: "lic", ...overrides }); + const { betterAuth } = await import("better-auth"); + const { memoryAdapter } = await import("better-auth/adapters/memory"); + const { genericOAuth } = await import("better-auth/plugins"); + const auth = betterAuth({ + baseURL: "https://app.formbricks.test", + secret: "sso-oidc-contract-secret-0123456789ab", + database: memoryAdapter({ user: [], session: [], account: [], verification: [] }), + plugins: [genericOAuth({ config: m.ssoGenericOAuthConfig })], + }); + return (await auth.$context).socialProviders.find((p) => p.id === providerId); + }; + + const oidcAtMicrosoft = { + OIDC_OAUTH_ENABLED: true, + OIDC_CLIENT_ID: "oidc-id", + OIDC_CLIENT_SECRET: "oidc-secret", + OIDC_ISSUER: "https://login.microsoftonline.com/common/v2.0", + }; + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + test("a forged id_token is never accepted as the identity", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => { + throw new Error("graph unreachable"); + }) + ); + + const provider = await initializedProvider("openid", oidcAtMicrosoft); + expect(await provider?.getUserInfo?.({ accessToken: "t", idToken: forgedIdToken } as never)).toBeNull(); + }); + + test("the identity is the Graph response, and Graph is actually called", async () => { + const graph = vi.fn(async () => ({ + ok: true, + json: async () => ({ sub: "graph-subject", email: "real@corp.test", name: "Real User" }), + })); + vi.stubGlobal("fetch", graph); + + const provider = await initializedProvider("openid", oidcAtMicrosoft); + const result = await provider?.getUserInfo?.({ + accessToken: "access-token", + idToken: forgedIdToken, + } as never); + + expect(graph).toHaveBeenCalledWith("https://graph.microsoft.com/oidc/userinfo", expect.anything()); + // Assert the Graph identity positively first: on its own, `not.toContain` passes for a `null` + // result, so the two negatives below would hold even if the provider resolved nothing at all. + expect(result?.user).toMatchObject({ email: "real@corp.test" }); + expect(result?.data).toMatchObject({ sub: "graph-subject", email: "real@corp.test" }); + expect(JSON.stringify(result)).not.toContain("forged-subject"); + expect(JSON.stringify(result)).not.toContain("attacker@evil.test"); + }); + + test("a normal OIDC issuer keeps discovery and the default profile path", async () => { + const m = await loadProviders({ + ENTERPRISE_LICENSE_KEY: "lic", + ...oidcAtMicrosoft, + OIDC_ISSUER: "https://idp.test", + }); + const oidc = m.ssoGenericOAuthConfig.find((c) => c.providerId === "openid"); + + expect(oidc?.getUserInfo).toBeUndefined(); + expect(oidc?.discoveryUrl).toBe("https://idp.test/.well-known/openid-configuration"); + }); + + /** + * The sweep result, pinned. `saml` also configures explicit endpoints with no discovery, which is + * the shape that exposed the other two — but BoxyHQ only mints an id_token for an OIDC-flow request + * (`requestedOIDCFlow` in Jackson's oauth controller), and this provider requests no scopes at all. + * No id_token means the shortcut cannot fire, so no override is needed. If someone ever adds + * `openid` to these scopes, this test fails and says why. + */ + test("saml requests no openid scope, so no id_token exists for the shortcut to read", async () => { + const m = await loadProviders({ ENTERPRISE_LICENSE_KEY: "lic", SAML_OAUTH_ENABLED: true }); + const saml = m.ssoGenericOAuthConfig.find((c) => c.providerId === "saml"); + + // Anchor the negatives: without this, an unregistered `saml` makes `saml?.scopes ?? []` an empty + // array and `saml?.discoveryUrl` undefined, so both assertions below would hold while proving + // nothing about the provider they are supposed to be describing. + if (!saml) throw new Error("saml provider not registered"); + + expect(saml.scopes ?? []).not.toContain("openid"); + expect(saml.discoveryUrl).toBeUndefined(); + }); +}); diff --git a/apps/web/modules/ee/sso/lib/better-auth-providers.ts b/apps/web/modules/ee/sso/lib/better-auth-providers.ts index 18ad8baf89dc..690375f5990c 100644 --- a/apps/web/modules/ee/sso/lib/better-auth-providers.ts +++ b/apps/web/modules/ee/sso/lib/better-auth-providers.ts @@ -267,6 +267,53 @@ const microsoftGraphUserInfo = async (tokens: { return null; } }; + +/** + * The endpoints Microsoft publishes for a given authority, configured explicitly so Better Auth never + * builds an id_token verification config for it (ENG-2750). Shared with the `openid` provider, which + * can be pointed at the same authorities and breaks identically when it is (ENG-2754). + * + * These are the values Microsoft's own discovery document returns for a multi-tenant authority, so + * configuring them by hand changes nothing about where the flow goes — only that we skip discovery. + * Skipping it is the sole lever for turning verification off: `GenericOAuthConfig` offers no way to + * opt out once a discovery document has supplied both `jwks_uri` and `issuer`. + * + * Skipping discovery is NOT sufficient on its own, though, and that half is easy to miss — it was + * missed here until review. Removing the verification config also removes the guard that would + * otherwise stop Better Auth reading identity straight out of the unverified id_token, which is why + * `getUserInfo` below is part of this shape rather than an optimisation. + */ +const microsoftExplicitEndpoints = (authority: string) => ({ + authorizationUrl: `https://login.microsoftonline.com/${authority}/oauth2/v2.0/authorize`, + tokenUrl: `https://login.microsoftonline.com/${authority}/oauth2/v2.0/token`, + userInfoUrl: MICROSOFT_GRAPH_USERINFO_URL, + // Not redundant with `userInfoUrl` — see microsoftGraphUserInfo. The URL alone leaves Better Auth's + // unverified-id_token shortcut in play; this override is what actually forces the Graph call, and it + // applies to every provider that reaches this branch (azuread and openid alike). + getUserInfo: microsoftGraphUserInfo, +}); + +/** + * The Microsoft multi-tenant authority a URL names, if it names one — e.g. an `OIDC_ISSUER` of + * `https://login.microsoftonline.com/common/v2.0` resolves to `common` (ENG-2754). + * + * Matched on the parsed host rather than a substring, so a lookalike host cannot be mistaken for + * Microsoft, and returns undefined for a concrete tenant (a GUID or verified domain) since those + * advertise a real issuer and must keep discovery. + */ +const microsoftTemplateIssuerAuthority = (issuerUrl: string | undefined): string | undefined => { + if (!issuerUrl?.trim()) return undefined; + let parsed: URL; + try { + parsed = new URL(issuerUrl.trim()); + } catch { + return undefined; + } + if (parsed.host.toLowerCase() !== "login.microsoftonline.com") return undefined; + const authority = parsed.pathname.split("/").find(Boolean)?.toLowerCase(); + return authority && AZURE_TEMPLATE_ISSUER_TENANTS.has(authority) ? authority : undefined; +}; + // Unset behaves exactly like `common`: Microsoft's multi-tenant authority, and the documented default. const azureTenant = AZUREAD_TENANT_ID?.trim() || "common"; const isAzureTemplateIssuerTenant = AZURE_TEMPLATE_ISSUER_TENANTS.has(azureTenant.toLowerCase()); @@ -291,18 +338,34 @@ if ( ); } const azureEndpoints = isAzureTemplateIssuerTenant - ? { - authorizationUrl: `https://login.microsoftonline.com/${azureAuthority}/oauth2/v2.0/authorize`, - tokenUrl: `https://login.microsoftonline.com/${azureAuthority}/oauth2/v2.0/token`, - userInfoUrl: MICROSOFT_GRAPH_USERINFO_URL, - // Not redundant with `userInfoUrl` — see microsoftGraphUserInfo. The URL alone leaves Better - // Auth's unverified-id_token shortcut in play; this is what actually forces the Graph call. - getUserInfo: microsoftGraphUserInfo, - } + ? microsoftExplicitEndpoints(azureAuthority) : { discoveryUrl: `https://login.microsoftonline.com/${azureAuthority}/v2.0/.well-known/openid-configuration`, }; +/** + * The generic OIDC provider hits the same wall when it is pointed at Microsoft (ENG-2754). + * + * `OIDC_ISSUER` is arbitrary operator input, so unlike azuread there is no set of values to enumerate + * — but the one issuer known to advertise a placeholder is Microsoft's, and we already know its + * endpoints. Anything else keeps discovery: OpenID Discovery §4.3 requires the advertised issuer to be + * identical to the one in the tokens, so a conforming provider cannot land here at all. + * + * Falls back rather than failing at init, matching how azuread treats the identical root cause: this + * configuration worked on 5.3 (1.6 never parsed the id_token), so refusing to start would be a harsher + * regression than the one being fixed. The warning names the better answer instead — the dedicated + * `AZUREAD_*` provider, which maps Entra's profile properly. + */ +const oidcTemplateIssuerAuthority = microsoftTemplateIssuerAuthority(OIDC_ISSUER); +if (ENTERPRISE_LICENSE_KEY && OIDC_OAUTH_ENABLED && oidcTemplateIssuerAuthority) { + logger.warn( + `OIDC_ISSUER points at Microsoft's "${oidcTemplateIssuerAuthority}" authority, whose discovery document advertises a placeholder issuer, so id_tokens cannot be verified against it. Skipping discovery for this provider and taking identity from the userinfo endpoint; the authority you configured still applies at sign-in. Prefer the dedicated AZUREAD_CLIENT_ID / AZUREAD_CLIENT_SECRET provider for Microsoft Entra ID.` + ); +} +const oidcEndpoints = oidcTemplateIssuerAuthority + ? microsoftExplicitEndpoints(oidcTemplateIssuerAuthority) + : { discoveryUrl: `${OIDC_ISSUER}/.well-known/openid-configuration` }; + export const ssoGenericOAuthConfig: GenericOAuthConfig[] = ENTERPRISE_LICENSE_KEY ? [ ...(AZURE_OAUTH_ENABLED @@ -345,7 +408,7 @@ export const ssoGenericOAuthConfig: GenericOAuthConfig[] = ENTERPRISE_LICENSE_KE providerId: "openid", clientId: OIDC_CLIENT_ID ?? "", clientSecret: OIDC_CLIENT_SECRET ?? "", - discoveryUrl: `${OIDC_ISSUER}/.well-known/openid-configuration`, + ...oidcEndpoints, scopes: ["openid", "email", "profile"], // Redundant since 1.7 defaults it to true, kept explicit (see azuread above). pkce: true, diff --git a/docs/README.md b/docs/README.md index 91c95bea2498..1a691fba9d67 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,10 +4,10 @@ This documentation is built using Mintlify. Here's how to run it locally and con ## Local Development -1. Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify): +1. Install the [Mintlify CLI](https://www.npmjs.com/package/mint): ```bash -npm i -g mintlify +npm i -g mint ``` 2. Clone the Formbricks repository and navigate to the docs folder: @@ -20,7 +20,7 @@ cd formbricks/docs 3. Run the documentation locally: ```bash -mintlify dev +mint dev ``` The documentation will be available at `http://localhost:3000`. @@ -33,6 +33,6 @@ The documentation will be available at `http://localhost:3000`. ### Troubleshooting -- If Mintlify dev isn't running, try `mintlify install` to reinstall dependencies -- If a page loads as a 404, ensure you're in the `docs` folder with the `mint.json` file +- If `mint dev` isn't running, try `mint update` to get the latest version of the CLI. If both `mint` and the legacy `mintlify` package are installed, uninstall `mintlify` +- If a page loads as a 404, ensure you're in the `docs` folder with the `docs.json` file - For other issues, please check our [Contributing Guidelines](https://github.com/formbricks/formbricks/blob/main/CONTRIBUTING.md) diff --git a/docs/development/contribution/contribution.mdx b/docs/development/contribution/contribution.mdx deleted file mode 100644 index ef10eea9a864..000000000000 --- a/docs/development/contribution/contribution.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: "Contribute to Formbricks" -description: "How to contribute to Formbricks" -icon: "code" ---- - -We’re excited that you want to contribute to Formbricks! There are many ways to help, including reporting issues, fixing bugs, adding new features, or improving documentation. - -#### How to Contribute - -- **Issues:** Found a bug? Facing deployment problems? Have user feedback? Report an issue for the fastest response. - -- **Feature Requests:** Have an idea? Open an issue, tag it as an **Enhancement**, and clearly describe the issue you're solving. - -- **Pull Requests (PRs):** Fork the repo, make your changes, and submit a PR. - - - For small fixes with 1-5 lines of code changes, go ahead! - - - For bigger changes, we currently don't have the capacity to facilitate them. - -#### Talk to Us First - -We highly recommend engaging with us on [**GitHub Discussions**](https://github.com/formbricks/formbricks/discussions) before submitting contributions. -This helps improve the chances of your PR being accepted while avoiding unnecessary work. - -#### Contributor License Agreement (CLA) - -To keep Formbricks sustainable, we require a **CLA** from all contributors. - -Once you open a PR, our **CLA bot** will prompt you to sign the agreement. We can only merge contributions after the CLA is signed. - -#### Setting Up Your Development Environment - -You can set up your environment using: - -- [**Gitpod**](/development/local-setup/gitpod) - -- [**GitHub Codespaces**](/development/local-setup/github-codespaces) - -- [**Local Machine Setup**](/development/local-setup) - -For junior developers, **Gitpod or GitHub Codespaces** are recommended as they allow you to start coding in minutes. - diff --git a/docs/development/local-setup/github-codespaces.mdx b/docs/development/local-setup/github-codespaces.mdx deleted file mode 100644 index 01b3e920d90f..000000000000 --- a/docs/development/local-setup/github-codespaces.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: GitHub Codespaces -description: How to set up Formbricks in a GitHub Codespaces environment -icon: "github" ---- - -### GitHub Codespaces Setup - -This guide outlines how to set up Formbricks in a **GitHub Codespaces** environment. - -**Requirements:** - -- A GitHub Codespace that has support for Node.JS, pnpm, and Docker. - -**Steps:** - -1. **Open your repository in GitHub Codespaces. If needed, clone the repository:** - - ```bash - git clone https://github.com/formbricks/formbricks && cd formbricks - ``` - -2. **Setup NodeJS with nvm (if not already configured):** - - ```bash - nvm install && nvm use - ``` - -3. **Install the dependencies:** - - ```bash - pnpm install - ``` - -4. **Create a development `.env` file and generate the required secrets:** - - ```bash - pnpm dev:setup - ``` - -5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):** - - This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js. - - ```bash - pnpm agents:update - ``` - -6. **Launch the development setup:** - ```bash - pnpm go - ``` - -Use the Codespaces port forwarding to access Formbricks at [http://localhost:3000](http://localhost:3000). - -Make sure your Codespaces port configuration is set to allow access to the app. diff --git a/docs/development/local-setup/gitpod.mdx b/docs/development/local-setup/gitpod.mdx deleted file mode 100644 index 07e0f7cd8d48..000000000000 --- a/docs/development/local-setup/gitpod.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Gitpod -description: How to set up Formbricks in a Gitpod workspace -icon: "code" ---- - -### Gitpod Setup - -This guide explains how to set up Formbricks in a **Gitpod** workspace. - -**Requirements:** - -- A Gitpod workspace with Node.JS, pnpm, and Docker support. - -**Steps:** - -1. **Open the repository in Gitpod. The workspace typically clones the repo automatically. If not:** - - ```bash - git clone https://github.com/formbricks/formbricks && cd formbricks - ``` - -2. **Setup NodeJS with nvm:** - - ```bash - nvm install && nvm use - ``` - -3. **Install dependencies:** - - ```bash - pnpm install - ``` - -4. **Create a development `.env` file and generate the required secrets:** - - ```bash - pnpm dev:setup - ``` - -5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):** - - This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js. - - ```bash - pnpm agents:update - ``` - -6. **Run the development setup:** - ```bash - pnpm go - ``` - -Access the running app via the forwarded port (typically [http://localhost:3000](http://localhost:3000) inside Gitpod). - -Check your Gitpod settings to ensure Docker is enabled if required. diff --git a/docs/development/local-setup/linux.mdx b/docs/development/local-setup/linux.mdx deleted file mode 100644 index 2de7c2e18638..000000000000 --- a/docs/development/local-setup/linux.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Linux -description: How to set up Formbricks on a Linux machine -icon: "linux" ---- - -### Local Machine Setup - Linux - -This guide is recommended for advanced users setting up Formbricks on a **Linux** machine. - -Here are the requirements for setting up Formbricks on Linux: - -- Node.JS (v20 recommended) -- [pnpm](https://pnpm.io/) -- [Docker](https://www.docker.com/) (to run PostgreSQL/MailHog) - -**Steps:** - -1. **Clone the repository and move into the directory:** - - ```bash - git clone https://github.com/formbricks/formbricks && cd formbricks - ``` - -2. **Setup NodeJS with nvm:** - - ```bash - nvm install && nvm use - ``` - -3. **Install NodeJS packages via pnpm:** - - ```bash - pnpm install - ``` - -4. **Create a development `.env` file and generate the required secrets:** - - ```bash - pnpm dev:setup - ``` - -5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):** - - This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js. - - ```bash - pnpm agents:update - ``` - -6. **Start the development setup:** - ```bash - pnpm go - ``` - -You can now access Formbricks at [http://localhost:3000](http://localhost:3000). - -Create a new account on first login as no default account is available. diff --git a/docs/development/local-setup/mac.mdx b/docs/development/local-setup/mac.mdx deleted file mode 100644 index d9953514f499..000000000000 --- a/docs/development/local-setup/mac.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Mac -description: How to set up Formbricks on a Mac machine -icon: "apple" ---- - -### Local Machine Setup - Mac - -This guide is recommended for advanced users setting up Formbricks on a **Mac** machine. - -**Requirements:** - -- Node.JS (v20 recommended) -- [pnpm](https://pnpm.io/) -- [Docker](https://www.docker.com/) - -**Steps:** - -1. **Clone the repository and change directory:** - - ```bash - git clone https://github.com/formbricks/formbricks && cd formbricks - ``` - -2. **Setup NodeJS with nvm:** - - ```bash - nvm install && nvm use - ``` - -3. **Install NodeJS packages with pnpm:** - - ```bash - pnpm install - ``` - -4. **Create a development `.env` file and generate the required secrets:** - - ```bash - pnpm dev:setup - ``` - -5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):** - - This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js. - - ```bash - pnpm agents:update - ``` - -6. **Start the development setup:** - ```bash - pnpm go - ``` - -Visit [http://localhost:3000](http://localhost:3000) to access Formbricks. - -Ensure you create a new account at first login. diff --git a/docs/development/local-setup/windows.mdx b/docs/development/local-setup/windows.mdx deleted file mode 100644 index 429da9220289..000000000000 --- a/docs/development/local-setup/windows.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Windows -description: How to set up Formbricks on a Windows machine -icon: "windows" ---- - -### Local Machine Setup - Windows - - - This guide is intended for **Windows** users. For the best experience, use **WSL2** since pure Windows is - not fully supported. - - -**Requirements:** - -- Node.JS (v20 recommended) via WSL2 -- [pnpm](https://pnpm.io/) -- [Docker](https://www.docker.com/) (ensure Docker Desktop is installed with WSL2 integration enabled) - -**Steps (Using WSL2):** - -1. **Open your WSL2 terminal and clone the repository:** - - ```bash - git clone https://github.com/formbricks/formbricks && cd formbricks - ``` - -2. **Setup NodeJS with nvm in WSL2:** - - ```bash - nvm install && nvm use - ``` - -3. **Install packages using pnpm:** - - ```bash - pnpm install - ``` - -4. **Create a development `.env` file and generate the required secrets:** - - ```bash - pnpm dev:setup - ``` - -5. **Generate the Next.js AGENTS.md file (optional, for AI-assisted development):** - - This step generates an `AGENTS.md` file at the repository root that provides Next.js documentation context for AI coding assistants (e.g. Cursor, GitHub Copilot). It runs `npx @next/codemod agents-md` under the hood. Re-run it whenever you upgrade Next.js. - - ```bash - pnpm agents:update - ``` - -6. **Start the development setup:** - ```bash - pnpm go - ``` - -Access Formbricks at [http://localhost:3000](http://localhost:3000). - -If you run into conflicts, ensure any local services (like PostgreSQL) are stopped. diff --git a/docs/development/overview.mdx b/docs/development/overview.mdx index 36ba84b6f773..568a33d5ae93 100644 --- a/docs/development/overview.mdx +++ b/docs/development/overview.mdx @@ -1,16 +1,35 @@ --- title: Overview -description: Learn how to setup formbricks locally and build custom integrations and services. +description: How Formbricks is built, and the standards its code is held to. icon: "code" --- -Welcome to the Development section of Formbricks! This guide is designed to help you get started with setting up the repository locally, contributing to the Formbricks codebase, and customizing it to suit your needs. +This section is for people working on the Formbricks codebase itself. It documents **how the platform is put together** and **the conventions its code follows** — not how to use Formbricks, which is what the rest of these docs are for. -Whether you're a seasoned developer or just getting started, you'll find valuable information on how to: +## What is here -- **Set Up Locally**: Step-by-step instructions to clone the repository, install dependencies, and run Formbricks on your local machine. -- **Contribute**: Guidelines on how to contribute to the codebase, including coding standards, submitting pull requests, and collaborating with other developers. -- **Customize**: Tips and tricks for customizing Formbricks to better fit your specific use cases, including modifying components and extending functionality. + + + The architecture: how the Next.js app, the API gateway, the MCP server and the database fit together, and how tenants are kept apart. + -Dive in and start building with Formbricks today! + + How the repository is organised, how code is formatted and named, how errors are handled, and what a review and a test are expected to cover. + + +## Running Formbricks locally + +Setup instructions live with the code, where they stay in step with it: + +- [README](https://github.com/formbricks/formbricks#readme) — prerequisites and the steps to get an instance running. +- [CONTRIBUTING.md](https://github.com/formbricks/formbricks/blob/main/CONTRIBUTING.md) — how to raise an issue, and what a good pull request looks like. +- [Open in Gitpod](https://gitpod.io/#https://github.com/formbricks/formbricks) — a configured workspace in the browser, if you would rather not install anything. + +If something goes wrong, [GitHub Discussions](https://github.com/formbricks/formbricks/discussions) is the fastest way to get help from people who have hit the same thing. + + + Looking to **run** Formbricks rather than develop it? See [Self-hosting](/self-hosting/overview). + Building **against** it? See the [API v2 reference](/api-v2-reference/introduction) and the + [SDK guides](/surveys/website-app-surveys/framework-guides). + diff --git a/docs/development/standards/practices/documentation.mdx b/docs/development/standards/practices/documentation.mdx index 631f613b6b23..bea1c9a3d3f3 100644 --- a/docs/development/standards/practices/documentation.mdx +++ b/docs/development/standards/practices/documentation.mdx @@ -82,7 +82,7 @@ icon: "appropriate-icon" ``` 2. **Navigation** - - Add new pages to the appropriate section in `docs/mint.json` + - Add new pages to the appropriate section in `docs/docs.json` - Follow the existing navigation structure - Include proper redirects if URLs change @@ -118,7 +118,7 @@ Important information goes here - Test documentation locally using Mintlify CLI: ```bash -mintlify dev +mint dev ``` 2. **Review Process** diff --git a/docs/development/support/troubleshooting.mdx b/docs/development/support/troubleshooting.mdx deleted file mode 100644 index 09064108c6d1..000000000000 --- a/docs/development/support/troubleshooting.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Troubleshooting" -description: "Here, you'll find help with common issues." -icon: "wrench" ---- - -## "The app doesn't work after Prisma migration" - -If the app doesn’t work after a Prisma migration, clear your browser’s storage and reload the page. This will force the app to fetch data from the server again. ![prisma](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738108186/image_dwm9hp.jpg) - -## "I ran 'pnpm i' but there seems to be an error with the packages" - -If you run `pnpm i` and get an error with the packages, try running `pnpm clean` followed by `pnpm i` again. This often solves the problem. - -## "I get a full-screen error with cryptic strings" - -This usually happens when the Formbricks Widget isn't correctly or completely built. - -```bash -pnpm build --filter=@formbricks/js - -// Run the app again -pnpm dev -``` - -## "My machine struggles with the repository" - -Since we're working with a monorepo structure, the repository can get quite big. If you're having trouble working with the repository, try the following: - -```bash helloWorld.js -pnpm dev --filter=@formbricks/web... -``` - -It’s better to use a single terminal with `pnpm dev` rather than having multiple open (one with the Formbricks app and one with the demo). - -## Error: "Uncaught (in promise) SyntaxError: Unexpected token !DOCTYPE ... is not valid JSON"![Syntax Error](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738109837/image_wbxv8k.jpg) - -If you see this error, it happens when the person connected to the widget is deleted. To fix it, log out of the test person and reload the page.![Reset person](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738110212/image_nvkpku.jpg) diff --git a/docs/docs.json b/docs/docs.json index 3eb5492d807f..1502bbdbf8ba 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -62,6 +62,7 @@ "group": "Platform Features", "pages": [ "platform/features/ai-features", + "platform/features/contacts", { "group": "Integrations", "icon": "bridge", @@ -102,7 +103,7 @@ "groups": [ { "group": "Overview", - "pages": ["surveys/overview"] + "pages": ["surveys/overview", "surveys/analysis/reading-results"] }, { "group": "Survey Features", @@ -116,6 +117,7 @@ "surveys/general-features/overwrite-styling", "surveys/general-features/hidden-fields", "surveys/general-features/limit-submissions", + "surveys/general-features/survey-scheduling", "surveys/general-features/multi-language-surveys", "surveys/general-features/partial-submissions", "surveys/general-features/recall", @@ -184,8 +186,10 @@ "icon": "question", "pages": [ "surveys/question-type/address", + "surveys/question-type/ces", "surveys/question-type/consent", "surveys/question-type/contact-info", + "surveys/question-type/csat", "surveys/question-type/date", "surveys/question-type/file-upload", "surveys/question-type/free-text", @@ -238,7 +242,14 @@ "unify-feedback/feedback-sources", "unify-feedback/feedback-records", "unify-feedback/enrichment", - "unify-feedback/dashboards-charts", + { + "group": "Dashboards & Charts", + "icon": "chart-line", + "pages": [ + "unify-feedback/dashboards-charts", + "unify-feedback/dashboards-charts/creating-your-first-charts" + ] + }, "unify-feedback/topics-subtopics" ] }, @@ -314,13 +325,15 @@ "self-hosting/configuration/auth-sso/keycloak-oidc", "self-hosting/configuration/auth-sso/azure-ad-oauth", "self-hosting/configuration/auth-sso/google-oauth", - "self-hosting/configuration/auth-sso/saml-sso" + "self-hosting/configuration/auth-sso/saml-sso", + "self-hosting/configuration/auth-sso/setup-saml-with-identity-providers" ] }, { "group": "Integrations", "icon": "bridge", "pages": [ + "self-hosting/configuration/integrations", "self-hosting/configuration/integrations/airtable", "self-hosting/configuration/integrations/google-sheets", "self-hosting/configuration/integrations/n8n", @@ -368,16 +381,6 @@ "group": "Development", "pages": ["development/overview"] }, - { - "group": "Local Setup", - "pages": [ - "development/local-setup/linux", - "development/local-setup/mac", - "development/local-setup/windows", - "development/local-setup/gitpod", - "development/local-setup/github-codespaces" - ] - }, { "group": "Technical Handbook", "pages": [ @@ -427,24 +430,6 @@ ] } ] - }, - { - "group": "Contributions", - "pages": ["development/contribution/contribution"] - }, - { - "group": "Guides", - "pages": [ - { - "group": "Auth & Provision", - "icon": "user-shield", - "pages": ["development/guides/auth-and-provision/setup-saml-with-identity-providers"] - } - ] - }, - { - "group": "Support", - "pages": ["development/support/troubleshooting"] } ], "tab": "Development" @@ -487,6 +472,38 @@ ] }, "redirects": [ + { + "destination": "/docs/development/overview", + "source": "/docs/development/local-setup/linux" + }, + { + "destination": "/docs/development/overview", + "source": "/docs/development/local-setup/mac" + }, + { + "destination": "/docs/development/overview", + "source": "/docs/development/local-setup/windows" + }, + { + "destination": "/docs/development/overview", + "source": "/docs/development/local-setup/gitpod" + }, + { + "destination": "/docs/development/overview", + "source": "/docs/development/local-setup/github-codespaces" + }, + { + "destination": "/docs/development/overview", + "source": "/docs/development/contribution/contribution" + }, + { + "destination": "/docs/development/overview", + "source": "/docs/development/support/troubleshooting" + }, + { + "destination": "/docs/self-hosting/configuration/auth-sso/setup-saml-with-identity-providers", + "source": "/docs/development/guides/auth-and-provision/setup-saml-with-identity-providers" + }, { "destination": "/docs/unify-feedback/feedback-datasets", "source": "/docs/unify-feedback/feedback-directories" @@ -1256,7 +1273,7 @@ "source": "/docs/developer-docs/rest-api" }, { - "destination": "/docs/development/contribution/contribution", + "destination": "/docs/development/overview", "source": "/docs/developer-docs/contributing/get-started" }, { @@ -1264,7 +1281,7 @@ "source": "/docs/api-docs" }, { - "destination": "/docs/development/support/troubleshooting", + "destination": "/docs/development/overview", "source": "/docs/developer-docs/contributing/troubleshooting" } ], diff --git a/docs/images/api-reference/add-api-key.webp b/docs/images/api-reference/add-api-key.webp deleted file mode 100644 index 5b24b7abad46..000000000000 Binary files a/docs/images/api-reference/add-api-key.webp and /dev/null differ diff --git a/docs/images/api-reference/api-keys.webp b/docs/images/api-reference/api-keys.webp deleted file mode 100644 index b94afe2a465a..000000000000 Binary files a/docs/images/api-reference/api-keys.webp and /dev/null differ diff --git a/docs/images/api-reference/copy-api-key.webp b/docs/images/api-reference/copy-api-key.webp deleted file mode 100644 index c21d947c4366..000000000000 Binary files a/docs/images/api-reference/copy-api-key.webp and /dev/null differ diff --git a/docs/images/api-reference/create-api-key.webp b/docs/images/api-reference/create-api-key.webp deleted file mode 100644 index 7f64d0830016..000000000000 Binary files a/docs/images/api-reference/create-api-key.webp and /dev/null differ diff --git a/docs/images/api-reference/organization-settings.webp b/docs/images/api-reference/organization-settings.webp deleted file mode 100644 index b5860d944b21..000000000000 Binary files a/docs/images/api-reference/organization-settings.webp and /dev/null differ diff --git a/docs/images/platform/features/contacts/attribute-keys.webp b/docs/images/platform/features/contacts/attribute-keys.webp new file mode 100644 index 000000000000..31887f930cd7 Binary files /dev/null and b/docs/images/platform/features/contacts/attribute-keys.webp differ diff --git a/docs/images/platform/features/contacts/contacts-list.webp b/docs/images/platform/features/contacts/contacts-list.webp new file mode 100644 index 000000000000..d16f31aecd5d Binary files /dev/null and b/docs/images/platform/features/contacts/contacts-list.webp differ diff --git a/docs/images/platform/features/contacts/segments.webp b/docs/images/platform/features/contacts/segments.webp new file mode 100644 index 000000000000..8474d9b96a24 Binary files /dev/null and b/docs/images/platform/features/contacts/segments.webp differ diff --git a/docs/images/platform/features/styling-theme/allow-overwrite.webp b/docs/images/platform/features/styling-theme/allow-overwrite.webp deleted file mode 100644 index fcadbc68e09c..000000000000 Binary files a/docs/images/platform/features/styling-theme/allow-overwrite.webp and /dev/null differ diff --git a/docs/images/platform/features/styling-theme/appearance.webp b/docs/images/platform/features/styling-theme/appearance.webp new file mode 100644 index 000000000000..5fbafa1edf70 Binary files /dev/null and b/docs/images/platform/features/styling-theme/appearance.webp differ diff --git a/docs/images/platform/features/styling-theme/enable-custom-styling.webp b/docs/images/platform/features/styling-theme/enable-custom-styling.webp new file mode 100644 index 000000000000..88a39c355741 Binary files /dev/null and b/docs/images/platform/features/styling-theme/enable-custom-styling.webp differ diff --git a/docs/images/platform/features/styling-theme/form-css-styling.webp b/docs/images/platform/features/styling-theme/form-css-styling.webp deleted file mode 100644 index 8a1bec036a46..000000000000 Binary files a/docs/images/platform/features/styling-theme/form-css-styling.webp and /dev/null differ diff --git a/docs/images/platform/features/styling-theme/logo.webp b/docs/images/platform/features/styling-theme/logo.webp new file mode 100644 index 000000000000..d7cb3e620427 Binary files /dev/null and b/docs/images/platform/features/styling-theme/logo.webp differ diff --git a/docs/images/platform/features/styling-theme/step-five.webp b/docs/images/platform/features/styling-theme/step-five.webp deleted file mode 100644 index 71daa7e7513f..000000000000 Binary files a/docs/images/platform/features/styling-theme/step-five.webp and /dev/null differ diff --git a/docs/images/platform/features/styling-theme/step-four.webp b/docs/images/platform/features/styling-theme/step-four.webp deleted file mode 100644 index 3ca7dc38d9fe..000000000000 Binary files a/docs/images/platform/features/styling-theme/step-four.webp and /dev/null differ diff --git a/docs/images/platform/features/styling-theme/step-seven.webp b/docs/images/platform/features/styling-theme/step-seven.webp deleted file mode 100644 index bcbabf831527..000000000000 Binary files a/docs/images/platform/features/styling-theme/step-seven.webp and /dev/null differ diff --git a/docs/images/platform/features/styling-theme/step-six.webp b/docs/images/platform/features/styling-theme/step-six.webp deleted file mode 100644 index 3ac271b6c8d0..000000000000 Binary files a/docs/images/platform/features/styling-theme/step-six.webp and /dev/null differ diff --git a/docs/images/platform/features/user-management/organizations-and-roles/members.webp b/docs/images/platform/features/user-management/organizations-and-roles/members.webp new file mode 100644 index 000000000000..3df45b2b3b17 Binary files /dev/null and b/docs/images/platform/features/user-management/organizations-and-roles/members.webp differ diff --git a/docs/images/platform/features/user-management/teams-and-roles/teams.webp b/docs/images/platform/features/user-management/teams-and-roles/teams.webp new file mode 100644 index 000000000000..d28825a7af21 Binary files /dev/null and b/docs/images/platform/features/user-management/teams-and-roles/teams.webp differ diff --git a/docs/images/platform/features/user-management/teams-and-roles/workspace-access.webp b/docs/images/platform/features/user-management/teams-and-roles/workspace-access.webp new file mode 100644 index 000000000000..f932abc789e5 Binary files /dev/null and b/docs/images/platform/features/user-management/teams-and-roles/workspace-access.webp differ diff --git a/docs/images/platform/features/user-management/two-factor-auth/setup.webp b/docs/images/platform/features/user-management/two-factor-auth/setup.webp new file mode 100644 index 000000000000..b6f61d33da86 Binary files /dev/null and b/docs/images/platform/features/user-management/two-factor-auth/setup.webp differ diff --git a/docs/images/self-hosting/advanced/powered-by-formbricks.webp b/docs/images/self-hosting/advanced/powered-by-formbricks.webp deleted file mode 100644 index 5d1ea0056eb5..000000000000 Binary files a/docs/images/self-hosting/advanced/powered-by-formbricks.webp and /dev/null differ diff --git a/docs/images/development/guides/auth-and-provision/okta/app-created.webp b/docs/images/self-hosting/configuration/auth-sso/okta/app-created.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/app-created.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/app-created.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/assign-to-people.webp b/docs/images/self-hosting/configuration/auth-sso/okta/assign-to-people.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/assign-to-people.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/assign-to-people.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/assignments-tab.webp b/docs/images/self-hosting/configuration/auth-sso/okta/assignments-tab.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/assignments-tab.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/assignments-tab.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/create-app-integration.webp b/docs/images/self-hosting/configuration/auth-sso/okta/create-app-integration.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/create-app-integration.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/create-app-integration.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/fields-mapping.webp b/docs/images/self-hosting/configuration/auth-sso/okta/fields-mapping.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/fields-mapping.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/fields-mapping.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/general-settings.webp b/docs/images/self-hosting/configuration/auth-sso/okta/general-settings.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/general-settings.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/general-settings.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/idp-metadata.webp b/docs/images/self-hosting/configuration/auth-sso/okta/idp-metadata.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/idp-metadata.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/idp-metadata.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/internal-app.webp b/docs/images/self-hosting/configuration/auth-sso/okta/internal-app.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/internal-app.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/internal-app.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/okta-applications.webp b/docs/images/self-hosting/configuration/auth-sso/okta/okta-applications.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/okta-applications.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/okta-applications.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/saml-integration-settings.webp b/docs/images/self-hosting/configuration/auth-sso/okta/saml-integration-settings.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/saml-integration-settings.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/saml-integration-settings.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/select-saml-2.0.webp b/docs/images/self-hosting/configuration/auth-sso/okta/select-saml-2.0.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/select-saml-2.0.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/select-saml-2.0.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/select-users.webp b/docs/images/self-hosting/configuration/auth-sso/okta/select-users.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/select-users.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/select-users.webp diff --git a/docs/images/development/guides/auth-and-provision/okta/view-saml-instructions.webp b/docs/images/self-hosting/configuration/auth-sso/okta/view-saml-instructions.webp similarity index 100% rename from docs/images/development/guides/auth-and-provision/okta/view-saml-instructions.webp rename to docs/images/self-hosting/configuration/auth-sso/okta/view-saml-instructions.webp diff --git a/docs/images/surveys/analysis/responses.webp b/docs/images/surveys/analysis/responses.webp new file mode 100644 index 000000000000..115bc40d1947 Binary files /dev/null and b/docs/images/surveys/analysis/responses.webp differ diff --git a/docs/images/surveys/analysis/summary.webp b/docs/images/surveys/analysis/summary.webp new file mode 100644 index 000000000000..b2b2f89ff177 Binary files /dev/null and b/docs/images/surveys/analysis/summary.webp differ diff --git a/docs/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-image.webp b/docs/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-image.webp deleted file mode 100644 index 838117dc321f..000000000000 Binary files a/docs/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-image.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-video.webp b/docs/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-video.webp deleted file mode 100644 index cb4b0536bcbb..000000000000 Binary files a/docs/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-video.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question.webp b/docs/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question.webp deleted file mode 100644 index 54ea67f3ff11..000000000000 Binary files a/docs/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/add-image-or-video-question/media-panel.webp b/docs/images/surveys/general-features/add-image-or-video-question/media-panel.webp new file mode 100644 index 000000000000..8fae90cb17de Binary files /dev/null and b/docs/images/surveys/general-features/add-image-or-video-question/media-panel.webp differ diff --git a/docs/images/surveys/general-features/add-image-or-video-question/media-video.webp b/docs/images/surveys/general-features/add-image-or-video-question/media-video.webp new file mode 100644 index 000000000000..f27116a95604 Binary files /dev/null and b/docs/images/surveys/general-features/add-image-or-video-question/media-video.webp differ diff --git a/docs/images/surveys/general-features/conditional-logic/action-calculate-operators.webp b/docs/images/surveys/general-features/conditional-logic/action-calculate-operators.webp deleted file mode 100644 index c0928693b3f8..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/action-calculate-operators.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/action-calculate-value.webp b/docs/images/surveys/general-features/conditional-logic/action-calculate-value.webp deleted file mode 100644 index 678a56d404a2..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/action-calculate-value.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/action-calculate-variables.webp b/docs/images/surveys/general-features/conditional-logic/action-calculate-variables.webp deleted file mode 100644 index b33a360a2ccb..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/action-calculate-variables.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/action-calculate.webp b/docs/images/surveys/general-features/conditional-logic/action-calculate.webp deleted file mode 100644 index fad23ba41676..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/action-calculate.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/action-jump.webp b/docs/images/surveys/general-features/conditional-logic/action-jump.webp deleted file mode 100644 index acae8eea84ed..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/action-jump.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/action-options.webp b/docs/images/surveys/general-features/conditional-logic/action-options.webp index 46351962005c..74743ad159d5 100644 Binary files a/docs/images/surveys/general-features/conditional-logic/action-options.webp and b/docs/images/surveys/general-features/conditional-logic/action-options.webp differ diff --git a/docs/images/surveys/general-features/conditional-logic/action-require.webp b/docs/images/surveys/general-features/conditional-logic/action-require.webp deleted file mode 100644 index 652dcbd7dde9..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/action-require.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/add-logic.webp b/docs/images/surveys/general-features/conditional-logic/add-logic.webp index 03504d21ca13..afb704cb9e75 100644 Binary files a/docs/images/surveys/general-features/conditional-logic/add-logic.webp and b/docs/images/surveys/general-features/conditional-logic/add-logic.webp differ diff --git a/docs/images/surveys/general-features/conditional-logic/condition-chaining.webp b/docs/images/surveys/general-features/conditional-logic/condition-chaining.webp deleted file mode 100644 index a5560aced8a2..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/condition-chaining.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/condition-operators.webp b/docs/images/surveys/general-features/conditional-logic/condition-operators.webp index 8d99b4d44e7b..d319fe3a927e 100644 Binary files a/docs/images/surveys/general-features/conditional-logic/condition-operators.webp and b/docs/images/surveys/general-features/conditional-logic/condition-operators.webp differ diff --git a/docs/images/surveys/general-features/conditional-logic/condition-options.webp b/docs/images/surveys/general-features/conditional-logic/condition-options.webp deleted file mode 100644 index c08fbfa2b4d2..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/condition-options.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/condition-value.webp b/docs/images/surveys/general-features/conditional-logic/condition-value.webp deleted file mode 100644 index 4bc2d51218d2..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/condition-value.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/conditions.webp b/docs/images/surveys/general-features/conditional-logic/conditions.webp deleted file mode 100644 index 85bb78d5967f..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/conditions.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/editor.webp b/docs/images/surveys/general-features/conditional-logic/editor.webp deleted file mode 100644 index bb7dab326c38..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/editor.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/conditional-logic/logic-rule.webp b/docs/images/surveys/general-features/conditional-logic/logic-rule.webp new file mode 100644 index 000000000000..7f9e0864975f Binary files /dev/null and b/docs/images/surveys/general-features/conditional-logic/logic-rule.webp differ diff --git a/docs/images/surveys/general-features/conditional-logic/question-logic.webp b/docs/images/surveys/general-features/conditional-logic/question-logic.webp deleted file mode 100644 index 9d1d48845fd3..000000000000 Binary files a/docs/images/surveys/general-features/conditional-logic/question-logic.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/email-followups/followups-tab.webp b/docs/images/surveys/general-features/email-followups/followups-tab.webp new file mode 100644 index 000000000000..59c34ceaa949 Binary files /dev/null and b/docs/images/surveys/general-features/email-followups/followups-tab.webp differ diff --git a/docs/images/surveys/general-features/email-followups/new-followup.webp b/docs/images/surveys/general-features/email-followups/new-followup.webp new file mode 100644 index 000000000000..0c9215392ea8 Binary files /dev/null and b/docs/images/surveys/general-features/email-followups/new-followup.webp differ diff --git a/docs/images/surveys/general-features/hidden-fields/editor.webp b/docs/images/surveys/general-features/hidden-fields/editor.webp new file mode 100644 index 000000000000..724ad88722b1 Binary files /dev/null and b/docs/images/surveys/general-features/hidden-fields/editor.webp differ diff --git a/docs/images/surveys/general-features/hidden-fields/filled-hidden-fields.webp b/docs/images/surveys/general-features/hidden-fields/filled-hidden-fields.webp deleted file mode 100644 index 1881351586b3..000000000000 Binary files a/docs/images/surveys/general-features/hidden-fields/filled-hidden-fields.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/hidden-fields/hidden-field-responses.webp b/docs/images/surveys/general-features/hidden-fields/hidden-field-responses.webp deleted file mode 100644 index 0d01d48e7461..000000000000 Binary files a/docs/images/surveys/general-features/hidden-fields/hidden-field-responses.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/hidden-fields/hidden-fields.webp b/docs/images/surveys/general-features/hidden-fields/hidden-fields.webp deleted file mode 100644 index 6c4dc4ab8dad..000000000000 Binary files a/docs/images/surveys/general-features/hidden-fields/hidden-fields.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/hidden-fields/input-hidden-fields.webp b/docs/images/surveys/general-features/hidden-fields/input-hidden-fields.webp deleted file mode 100644 index 8f3d4e45a9c8..000000000000 Binary files a/docs/images/surveys/general-features/hidden-fields/input-hidden-fields.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/hidden-fields/responses.webp b/docs/images/surveys/general-features/hidden-fields/responses.webp new file mode 100644 index 000000000000..e64d03f8a775 Binary files /dev/null and b/docs/images/surveys/general-features/hidden-fields/responses.webp differ diff --git a/docs/images/surveys/general-features/hide-back-button/hide-back-button.webp b/docs/images/surveys/general-features/hide-back-button/hide-back-button.webp deleted file mode 100644 index 88c1a8bca355..000000000000 Binary files a/docs/images/surveys/general-features/hide-back-button/hide-back-button.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/hide-back-button/response-option.webp b/docs/images/surveys/general-features/hide-back-button/response-option.webp new file mode 100644 index 000000000000..23fae6be4948 Binary files /dev/null and b/docs/images/surveys/general-features/hide-back-button/response-option.webp differ diff --git a/docs/images/surveys/general-features/limit-submissions/limit-submissions.webp b/docs/images/surveys/general-features/limit-submissions/limit-submissions.webp deleted file mode 100644 index 4dcbe47395f0..000000000000 Binary files a/docs/images/surveys/general-features/limit-submissions/limit-submissions.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/limit-submissions/response-option.webp b/docs/images/surveys/general-features/limit-submissions/response-option.webp new file mode 100644 index 000000000000..e77e697f3571 Binary files /dev/null and b/docs/images/surveys/general-features/limit-submissions/response-option.webp differ diff --git a/docs/images/surveys/general-features/metadata/filters.webp b/docs/images/surveys/general-features/metadata/filters.webp deleted file mode 100644 index 8c9d2b6f6bf5..000000000000 Binary files a/docs/images/surveys/general-features/metadata/filters.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/metadata/metadata-card.webp b/docs/images/surveys/general-features/metadata/metadata-card.webp deleted file mode 100644 index 16c802f7b290..000000000000 Binary files a/docs/images/surveys/general-features/metadata/metadata-card.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/metadata/response-metadata.webp b/docs/images/surveys/general-features/metadata/response-metadata.webp new file mode 100644 index 000000000000..351a711e2442 Binary files /dev/null and b/docs/images/surveys/general-features/metadata/response-metadata.webp differ diff --git a/docs/images/surveys/general-features/overwrite-styling/custom-styles.webp b/docs/images/surveys/general-features/overwrite-styling/custom-styles.webp new file mode 100644 index 000000000000..7cf8ac0dcffd Binary files /dev/null and b/docs/images/surveys/general-features/overwrite-styling/custom-styles.webp differ diff --git a/docs/images/surveys/general-features/overwrite-styling/step-eleven.webp b/docs/images/surveys/general-features/overwrite-styling/step-eleven.webp deleted file mode 100644 index 28fbade6c850..000000000000 Binary files a/docs/images/surveys/general-features/overwrite-styling/step-eleven.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/overwrite-styling/step-nine.webp b/docs/images/surveys/general-features/overwrite-styling/step-nine.webp deleted file mode 100644 index bd8c73fe7e9e..000000000000 Binary files a/docs/images/surveys/general-features/overwrite-styling/step-nine.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/overwrite-styling/step-ten.webp b/docs/images/surveys/general-features/overwrite-styling/step-ten.webp deleted file mode 100644 index 092b337eb7e8..000000000000 Binary files a/docs/images/surveys/general-features/overwrite-styling/step-ten.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/overwrite-styling/styling-tab.webp b/docs/images/surveys/general-features/overwrite-styling/styling-tab.webp new file mode 100644 index 000000000000..38acecb97970 Binary files /dev/null and b/docs/images/surveys/general-features/overwrite-styling/styling-tab.webp differ diff --git a/docs/images/surveys/general-features/quota-management/quotas.webp b/docs/images/surveys/general-features/quota-management/quotas.webp new file mode 100644 index 000000000000..e94955e3b3a7 Binary files /dev/null and b/docs/images/surveys/general-features/quota-management/quotas.webp differ diff --git a/docs/images/surveys/general-features/recall/fallback.webp b/docs/images/surveys/general-features/recall/fallback.webp new file mode 100644 index 000000000000..94b8534a2b40 Binary files /dev/null and b/docs/images/surveys/general-features/recall/fallback.webp differ diff --git a/docs/images/surveys/general-features/recall/recall-menu.webp b/docs/images/surveys/general-features/recall/recall-menu.webp new file mode 100644 index 000000000000..91f7ea9e9abb Binary files /dev/null and b/docs/images/surveys/general-features/recall/recall-menu.webp differ diff --git a/docs/images/surveys/general-features/recall/step-one.webp b/docs/images/surveys/general-features/recall/step-one.webp deleted file mode 100644 index 42ebcd0fa5eb..000000000000 Binary files a/docs/images/surveys/general-features/recall/step-one.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/recall/step-three.webp b/docs/images/surveys/general-features/recall/step-three.webp deleted file mode 100644 index dd630ba87627..000000000000 Binary files a/docs/images/surveys/general-features/recall/step-three.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/recall/step-two.webp b/docs/images/surveys/general-features/recall/step-two.webp deleted file mode 100644 index 98761acc8cbd..000000000000 Binary files a/docs/images/surveys/general-features/recall/step-two.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/spam-protection/response-option.webp b/docs/images/surveys/general-features/spam-protection/response-option.webp new file mode 100644 index 000000000000..5ea15a4dcbe1 Binary files /dev/null and b/docs/images/surveys/general-features/spam-protection/response-option.webp differ diff --git a/docs/images/surveys/general-features/spam-protection/spam-protection.webp b/docs/images/surveys/general-features/spam-protection/spam-protection.webp deleted file mode 100644 index e65cd6cf8a10..000000000000 Binary files a/docs/images/surveys/general-features/spam-protection/spam-protection.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/survey-scheduling/publish-and-close.webp b/docs/images/surveys/general-features/survey-scheduling/publish-and-close.webp new file mode 100644 index 000000000000..3d2add19858a Binary files /dev/null and b/docs/images/surveys/general-features/survey-scheduling/publish-and-close.webp differ diff --git a/docs/images/surveys/general-features/tags/manager.webp b/docs/images/surveys/general-features/tags/manager.webp new file mode 100644 index 000000000000..0df6a364b499 Binary files /dev/null and b/docs/images/surveys/general-features/tags/manager.webp differ diff --git a/docs/images/surveys/general-features/validation-rules/editor.webp b/docs/images/surveys/general-features/validation-rules/editor.webp new file mode 100644 index 000000000000..d8effb6bc856 Binary files /dev/null and b/docs/images/surveys/general-features/validation-rules/editor.webp differ diff --git a/docs/images/surveys/general-features/variables/created-variables.webp b/docs/images/surveys/general-features/variables/created-variables.webp deleted file mode 100644 index 4567d9159869..000000000000 Binary files a/docs/images/surveys/general-features/variables/created-variables.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/variables/editor.webp b/docs/images/surveys/general-features/variables/editor.webp new file mode 100644 index 000000000000..b306a6100a6f Binary files /dev/null and b/docs/images/surveys/general-features/variables/editor.webp differ diff --git a/docs/images/surveys/general-features/variables/input-variables.webp b/docs/images/surveys/general-features/variables/input-variables.webp deleted file mode 100644 index 6273bfe5fc44..000000000000 Binary files a/docs/images/surveys/general-features/variables/input-variables.webp and /dev/null differ diff --git a/docs/images/surveys/general-features/variables/variables-card.webp b/docs/images/surveys/general-features/variables/variables-card.webp deleted file mode 100644 index c9f6c781d65a..000000000000 Binary files a/docs/images/surveys/general-features/variables/variables-card.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/data-prefilling/question-id.webp b/docs/images/surveys/link-surveys/data-prefilling/question-id.webp index 8dfcb219c500..7055da545429 100644 Binary files a/docs/images/surveys/link-surveys/data-prefilling/question-id.webp and b/docs/images/surveys/link-surveys/data-prefilling/question-id.webp differ diff --git a/docs/images/surveys/link-surveys/link-settings/link-settings.webp b/docs/images/surveys/link-surveys/link-settings/link-settings.webp index 3e04886a30ff..d27083f5fd38 100644 Binary files a/docs/images/surveys/link-surveys/link-settings/link-settings.webp and b/docs/images/surveys/link-surveys/link-settings/link-settings.webp differ diff --git a/docs/images/surveys/link-surveys/personal-links/generate.webp b/docs/images/surveys/link-surveys/personal-links/generate.webp new file mode 100644 index 000000000000..b0973493f295 Binary files /dev/null and b/docs/images/surveys/link-surveys/personal-links/generate.webp differ diff --git a/docs/images/surveys/link-surveys/pin-protected-surveys/pin-prompt.webp b/docs/images/surveys/link-surveys/pin-protected-surveys/pin-prompt.webp new file mode 100644 index 000000000000..c9a56187f840 Binary files /dev/null and b/docs/images/surveys/link-surveys/pin-protected-surveys/pin-prompt.webp differ diff --git a/docs/images/surveys/link-surveys/pin-protected-surveys/response-option.webp b/docs/images/surveys/link-surveys/pin-protected-surveys/response-option.webp new file mode 100644 index 000000000000..c33edef484e9 Binary files /dev/null and b/docs/images/surveys/link-surveys/pin-protected-surveys/response-option.webp differ diff --git a/docs/images/surveys/link-surveys/pin-protected-surveys/step-five.webp b/docs/images/surveys/link-surveys/pin-protected-surveys/step-five.webp deleted file mode 100644 index 52c23b53debb..000000000000 Binary files a/docs/images/surveys/link-surveys/pin-protected-surveys/step-five.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/pin-protected-surveys/step-four.webp b/docs/images/surveys/link-surveys/pin-protected-surveys/step-four.webp deleted file mode 100644 index b186fde5f44e..000000000000 Binary files a/docs/images/surveys/link-surveys/pin-protected-surveys/step-four.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/pin-protected-surveys/step-one.webp b/docs/images/surveys/link-surveys/pin-protected-surveys/step-one.webp deleted file mode 100644 index 7d74be7026fc..000000000000 Binary files a/docs/images/surveys/link-surveys/pin-protected-surveys/step-one.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/pin-protected-surveys/step-three.webp b/docs/images/surveys/link-surveys/pin-protected-surveys/step-three.webp deleted file mode 100644 index c59f807ce8d2..000000000000 Binary files a/docs/images/surveys/link-surveys/pin-protected-surveys/step-three.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/pin-protected-surveys/step-two.webp b/docs/images/surveys/link-surveys/pin-protected-surveys/step-two.webp deleted file mode 100644 index abe1ae4de422..000000000000 Binary files a/docs/images/surveys/link-surveys/pin-protected-surveys/step-two.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/pretty-url/custom-slug.webp b/docs/images/surveys/link-surveys/pretty-url/custom-slug.webp new file mode 100644 index 000000000000..bac392d668ce Binary files /dev/null and b/docs/images/surveys/link-surveys/pretty-url/custom-slug.webp differ diff --git a/docs/images/surveys/link-surveys/single-use-links/single-use-links.webp b/docs/images/surveys/link-surveys/single-use-links/single-use-links.webp index 98e74ce34982..6e82e9704b1a 100644 Binary files a/docs/images/surveys/link-surveys/single-use-links/single-use-links.webp and b/docs/images/surveys/link-surveys/single-use-links/single-use-links.webp differ diff --git a/docs/images/surveys/link-surveys/source-tracking/responses-table.webp b/docs/images/surveys/link-surveys/source-tracking/responses-table.webp new file mode 100644 index 000000000000..351a711e2442 Binary files /dev/null and b/docs/images/surveys/link-surveys/source-tracking/responses-table.webp differ diff --git a/docs/images/surveys/link-surveys/source-tracking/view-response.webp b/docs/images/surveys/link-surveys/source-tracking/view-response.webp deleted file mode 100644 index 8b0e9bd7d5a9..000000000000 Binary files a/docs/images/surveys/link-surveys/source-tracking/view-response.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/verify-email-before-survey/email-gate.webp b/docs/images/surveys/link-surveys/verify-email-before-survey/email-gate.webp new file mode 100644 index 000000000000..0a9a2bcaf01f Binary files /dev/null and b/docs/images/surveys/link-surveys/verify-email-before-survey/email-gate.webp differ diff --git a/docs/images/surveys/link-surveys/verify-email-before-survey/response-option.webp b/docs/images/surveys/link-surveys/verify-email-before-survey/response-option.webp new file mode 100644 index 000000000000..3268aff84bfb Binary files /dev/null and b/docs/images/surveys/link-surveys/verify-email-before-survey/response-option.webp differ diff --git a/docs/images/surveys/link-surveys/verify-email-before-survey/step-five.webp b/docs/images/surveys/link-surveys/verify-email-before-survey/step-five.webp deleted file mode 100644 index bce405556c2e..000000000000 Binary files a/docs/images/surveys/link-surveys/verify-email-before-survey/step-five.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/verify-email-before-survey/step-four.webp b/docs/images/surveys/link-surveys/verify-email-before-survey/step-four.webp deleted file mode 100644 index d9ed057e016c..000000000000 Binary files a/docs/images/surveys/link-surveys/verify-email-before-survey/step-four.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/verify-email-before-survey/step-one.webp b/docs/images/surveys/link-surveys/verify-email-before-survey/step-one.webp deleted file mode 100644 index 679f4e9e731d..000000000000 Binary files a/docs/images/surveys/link-surveys/verify-email-before-survey/step-one.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/verify-email-before-survey/step-seven.webp b/docs/images/surveys/link-surveys/verify-email-before-survey/step-seven.webp deleted file mode 100644 index 519720f79b44..000000000000 Binary files a/docs/images/surveys/link-surveys/verify-email-before-survey/step-seven.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/verify-email-before-survey/step-six.webp b/docs/images/surveys/link-surveys/verify-email-before-survey/step-six.webp deleted file mode 100644 index 5ca465d17183..000000000000 Binary files a/docs/images/surveys/link-surveys/verify-email-before-survey/step-six.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/verify-email-before-survey/step-three.webp b/docs/images/surveys/link-surveys/verify-email-before-survey/step-three.webp deleted file mode 100644 index b4e20a69b2e1..000000000000 Binary files a/docs/images/surveys/link-surveys/verify-email-before-survey/step-three.webp and /dev/null differ diff --git a/docs/images/surveys/link-surveys/verify-email-before-survey/step-two.webp b/docs/images/surveys/link-surveys/verify-email-before-survey/step-two.webp deleted file mode 100644 index a3d871c6687f..000000000000 Binary files a/docs/images/surveys/link-surveys/verify-email-before-survey/step-two.webp and /dev/null differ diff --git a/docs/images/surveys/question-type/ces/editor.webp b/docs/images/surveys/question-type/ces/editor.webp new file mode 100644 index 000000000000..ece647a9c922 Binary files /dev/null and b/docs/images/surveys/question-type/ces/editor.webp differ diff --git a/docs/images/surveys/question-type/csat/editor.webp b/docs/images/surveys/question-type/csat/editor.webp new file mode 100644 index 000000000000..f697eeb05afe Binary files /dev/null and b/docs/images/surveys/question-type/csat/editor.webp differ diff --git a/docs/images/surveys/website-app-surveys/actions/actions-view.webp b/docs/images/surveys/website-app-surveys/actions/actions-view.webp deleted file mode 100644 index 873b62f49979..000000000000 Binary files a/docs/images/surveys/website-app-surveys/actions/actions-view.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/actions/add-action.webp b/docs/images/surveys/website-app-surveys/actions/add-action.webp new file mode 100644 index 000000000000..ae4d6ab0d972 Binary files /dev/null and b/docs/images/surveys/website-app-surveys/actions/add-action.webp differ diff --git a/docs/images/surveys/website-app-surveys/actions/click-action.webp b/docs/images/surveys/website-app-surveys/actions/click-action.webp deleted file mode 100644 index a19de4ca4e5f..000000000000 Binary files a/docs/images/surveys/website-app-surveys/actions/click-action.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/actions/code-action.webp b/docs/images/surveys/website-app-surveys/actions/code-action.webp index 80e608fa2db6..068c832679a7 100644 Binary files a/docs/images/surveys/website-app-surveys/actions/code-action.webp and b/docs/images/surveys/website-app-surveys/actions/code-action.webp differ diff --git a/docs/images/surveys/website-app-surveys/actions/exit-intent.webp b/docs/images/surveys/website-app-surveys/actions/exit-intent.webp deleted file mode 100644 index 79fc0a2c6f30..000000000000 Binary files a/docs/images/surveys/website-app-surveys/actions/exit-intent.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/actions/i2.webp b/docs/images/surveys/website-app-surveys/actions/i2.webp deleted file mode 100644 index 0e12783e6710..000000000000 Binary files a/docs/images/surveys/website-app-surveys/actions/i2.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/actions/page-view.webp b/docs/images/surveys/website-app-surveys/actions/page-view.webp deleted file mode 100644 index 8cee3555b003..000000000000 Binary files a/docs/images/surveys/website-app-surveys/actions/page-view.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/actions/scroll.webp b/docs/images/surveys/website-app-surveys/actions/scroll.webp deleted file mode 100644 index 1fbda08a4f65..000000000000 Binary files a/docs/images/surveys/website-app-surveys/actions/scroll.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/actions/survey-trigger.webp b/docs/images/surveys/website-app-surveys/actions/survey-trigger.webp new file mode 100644 index 000000000000..2f23e3240038 Binary files /dev/null and b/docs/images/surveys/website-app-surveys/actions/survey-trigger.webp differ diff --git a/docs/images/surveys/website-app-surveys/actions/user-actions.webp b/docs/images/surveys/website-app-surveys/actions/user-actions.webp new file mode 100644 index 000000000000..2866f83f21c7 Binary files /dev/null and b/docs/images/surveys/website-app-surveys/actions/user-actions.webp differ diff --git a/docs/images/surveys/website-app-surveys/cooldown-period/workspace-setting.webp b/docs/images/surveys/website-app-surveys/cooldown-period/workspace-setting.webp new file mode 100644 index 000000000000..6c0e585dbb6c Binary files /dev/null and b/docs/images/surveys/website-app-surveys/cooldown-period/workspace-setting.webp differ diff --git a/docs/images/surveys/website-app-surveys/recontact/app-survey.webp b/docs/images/surveys/website-app-surveys/recontact/app-survey.webp deleted file mode 100644 index 467e536c8a16..000000000000 Binary files a/docs/images/surveys/website-app-surveys/recontact/app-survey.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/recontact/global-wait-time.webp b/docs/images/surveys/website-app-surveys/recontact/global-wait-time.webp deleted file mode 100644 index 6208dfa66bd3..000000000000 Binary files a/docs/images/surveys/website-app-surveys/recontact/global-wait-time.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/recontact/ignore-wait-time.webp b/docs/images/surveys/website-app-surveys/recontact/ignore-wait-time.webp deleted file mode 100644 index 97809d77c268..000000000000 Binary files a/docs/images/surveys/website-app-surveys/recontact/ignore-wait-time.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/recontact/survey-recontact.webp b/docs/images/surveys/website-app-surveys/recontact/survey-recontact.webp deleted file mode 100644 index 552871ab318f..000000000000 Binary files a/docs/images/surveys/website-app-surveys/recontact/survey-recontact.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/recontact/visibility-and-recontact.webp b/docs/images/surveys/website-app-surveys/recontact/visibility-and-recontact.webp new file mode 100644 index 000000000000..f41844d1df0d Binary files /dev/null and b/docs/images/surveys/website-app-surveys/recontact/visibility-and-recontact.webp differ diff --git a/docs/images/surveys/website-app-surveys/show-survey-to-percent-of-users/display-settings.webp b/docs/images/surveys/website-app-surveys/show-survey-to-percent-of-users/display-settings.webp new file mode 100644 index 000000000000..bc7b853e439f Binary files /dev/null and b/docs/images/surveys/website-app-surveys/show-survey-to-percent-of-users/display-settings.webp differ diff --git a/docs/images/surveys/website-app-surveys/targeting/add-filter.webp b/docs/images/surveys/website-app-surveys/targeting/add-filter.webp new file mode 100644 index 000000000000..b8bb2c937b2f Binary files /dev/null and b/docs/images/surveys/website-app-surveys/targeting/add-filter.webp differ diff --git a/docs/images/surveys/website-app-surveys/targeting/device-filter.webp b/docs/images/surveys/website-app-surveys/targeting/device-filter.webp deleted file mode 100644 index a4c388e037c4..000000000000 Binary files a/docs/images/surveys/website-app-surveys/targeting/device-filter.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/targeting/percentage.webp b/docs/images/surveys/website-app-surveys/targeting/percentage.webp deleted file mode 100644 index ff4fef841f1b..000000000000 Binary files a/docs/images/surveys/website-app-surveys/targeting/percentage.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/targeting/segment-editor.webp b/docs/images/surveys/website-app-surveys/targeting/segment-editor.webp new file mode 100644 index 000000000000..a4c2fa2912d8 Binary files /dev/null and b/docs/images/surveys/website-app-surveys/targeting/segment-editor.webp differ diff --git a/docs/images/surveys/website-app-surveys/targeting/segments-filter.webp b/docs/images/surveys/website-app-surveys/targeting/segments-filter.webp deleted file mode 100644 index 5129db253934..000000000000 Binary files a/docs/images/surveys/website-app-surveys/targeting/segments-filter.webp and /dev/null differ diff --git a/docs/images/surveys/website-app-surveys/targeting/segments-list.webp b/docs/images/surveys/website-app-surveys/targeting/segments-list.webp new file mode 100644 index 000000000000..32ef07eefad5 Binary files /dev/null and b/docs/images/surveys/website-app-surveys/targeting/segments-list.webp differ diff --git a/docs/images/surveys/website-app-surveys/targeting/survey-type.webp b/docs/images/surveys/website-app-surveys/targeting/survey-type.webp index 7c4361a71e2b..ea52c428afdd 100644 Binary files a/docs/images/surveys/website-app-surveys/targeting/survey-type.webp and b/docs/images/surveys/website-app-surveys/targeting/survey-type.webp differ diff --git a/docs/images/surveys/website-app-surveys/targeting/target-audience.webp b/docs/images/surveys/website-app-surveys/targeting/target-audience.webp index da7e8d5080eb..1108d8764e05 100644 Binary files a/docs/images/surveys/website-app-surveys/targeting/target-audience.webp and b/docs/images/surveys/website-app-surveys/targeting/target-audience.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/add-feedback-source.webp b/docs/images/unify-feedback/dashboards-charts/add-feedback-source.webp new file mode 100644 index 000000000000..da27dcedd67d Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/add-feedback-source.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/chart-nps-over-time.webp b/docs/images/unify-feedback/dashboards-charts/chart-nps-over-time.webp new file mode 100644 index 000000000000..44b873b9464a Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/chart-nps-over-time.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/chart-nps-vs-csat.webp b/docs/images/unify-feedback/dashboards-charts/chart-nps-vs-csat.webp new file mode 100644 index 000000000000..0f2042cb19f8 Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/chart-nps-vs-csat.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/chart-overall-nps.webp b/docs/images/unify-feedback/dashboards-charts/chart-overall-nps.webp new file mode 100644 index 000000000000..0a0a18469339 Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/chart-overall-nps.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/chart-promoters.webp b/docs/images/unify-feedback/dashboards-charts/chart-promoters.webp new file mode 100644 index 000000000000..2edd67792a9f Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/chart-promoters.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/chart-sentiment.webp b/docs/images/unify-feedback/dashboards-charts/chart-sentiment.webp new file mode 100644 index 000000000000..3e1454f922dc Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/chart-sentiment.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/create-chart.webp b/docs/images/unify-feedback/dashboards-charts/create-chart.webp new file mode 100644 index 000000000000..e2a394e737ef Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/create-chart.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/create-dataset.webp b/docs/images/unify-feedback/dashboards-charts/create-dataset.webp new file mode 100644 index 000000000000..d78d695eb97f Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/create-dataset.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/dashboard.webp b/docs/images/unify-feedback/dashboards-charts/dashboard.webp new file mode 100644 index 000000000000..acb7ac3e63b2 Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/dashboard.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/feedback-datasets.webp b/docs/images/unify-feedback/dashboards-charts/feedback-datasets.webp new file mode 100644 index 000000000000..f1c5dd4ed53a Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/feedback-datasets.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/feedback-records.webp b/docs/images/unify-feedback/dashboards-charts/feedback-records.webp new file mode 100644 index 000000000000..005c4179226a Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/feedback-records.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/feedback-sources.webp b/docs/images/unify-feedback/dashboards-charts/feedback-sources.webp new file mode 100644 index 000000000000..e6f4da4fb6c5 Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/feedback-sources.webp differ diff --git a/docs/images/unify-feedback/dashboards-charts/no-dataset-linked.webp b/docs/images/unify-feedback/dashboards-charts/no-dataset-linked.webp new file mode 100644 index 000000000000..c2b78356ead4 Binary files /dev/null and b/docs/images/unify-feedback/dashboards-charts/no-dataset-linked.webp differ diff --git a/docs/images/xm-and-surveys/core-features/access-roles/organization-settings-menu.webp b/docs/images/xm-and-surveys/core-features/access-roles/organization-settings-menu.webp deleted file mode 100644 index 63794142c6b8..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/access-roles/organization-settings-menu.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/access-roles/team-settings-menu.webp b/docs/images/xm-and-surveys/core-features/access-roles/team-settings-menu.webp deleted file mode 100644 index 63794142c6b8..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/access-roles/team-settings-menu.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/email-followups/duplicate-follow-up.webp b/docs/images/xm-and-surveys/core-features/email-followups/duplicate-follow-up.webp deleted file mode 100644 index cabe2e2ba7db..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/email-followups/duplicate-follow-up.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/email-followups/followup-content.webp b/docs/images/xm-and-surveys/core-features/email-followups/followup-content.webp deleted file mode 100644 index 4c73e6bcf440..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/email-followups/followup-content.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/email-followups/followup-form.webp b/docs/images/xm-and-surveys/core-features/email-followups/followup-form.webp deleted file mode 100644 index 666cc32ea877..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/email-followups/followup-form.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/email-followups/followup-recipient.webp b/docs/images/xm-and-surveys/core-features/email-followups/followup-recipient.webp deleted file mode 100644 index 1a45c14a5cc5..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/email-followups/followup-recipient.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/email-followups/followups-tab.webp b/docs/images/xm-and-surveys/core-features/email-followups/followups-tab.webp deleted file mode 100644 index 6459a8e66ce9..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/email-followups/followups-tab.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-google-sheet.webp b/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-google-sheet.webp deleted file mode 100644 index 824e56083954..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/integrations/activepieces/select-google-sheet.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/airtable/create-new-integration.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/create-new-integration.webp deleted file mode 100644 index a55bdc618db4..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/integrations/airtable/create-new-integration.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/airtable/open-developer-hub.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/open-developer-hub.webp deleted file mode 100644 index 648aab65989c..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/integrations/airtable/open-developer-hub.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/airtable/register-new-integration.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/register-new-integration.webp deleted file mode 100644 index 331ea78dfdea..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/integrations/airtable/register-new-integration.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/integrations/airtable/select-scopes.webp b/docs/images/xm-and-surveys/core-features/integrations/airtable/select-scopes.webp deleted file mode 100644 index f60e8ca7f43e..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/integrations/airtable/select-scopes.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/address.webp b/docs/images/xm-and-surveys/core-features/question-type/address.webp deleted file mode 100644 index 7f9279616f55..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/address.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/consent.webp b/docs/images/xm-and-surveys/core-features/question-type/consent.webp deleted file mode 100644 index 60291a402c33..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/consent.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/contact-info.webp b/docs/images/xm-and-surveys/core-features/question-type/contact-info.webp deleted file mode 100644 index ac2ade0722a2..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/contact-info.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/date.webp b/docs/images/xm-and-surveys/core-features/question-type/date.webp deleted file mode 100644 index 6fa0c564a460..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/date.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/file-upload.webp b/docs/images/xm-and-surveys/core-features/question-type/file-upload.webp deleted file mode 100644 index dd85a6d16162..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/file-upload.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/free-text.webp b/docs/images/xm-and-surveys/core-features/question-type/free-text.webp deleted file mode 100644 index 960d9b4cc13f..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/free-text.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/matrix.webp b/docs/images/xm-and-surveys/core-features/question-type/matrix.webp deleted file mode 100644 index 8c3d1c436f71..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/matrix.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/multi-select.webp b/docs/images/xm-and-surveys/core-features/question-type/multi-select.webp deleted file mode 100644 index 8c169b6f2e1f..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/multi-select.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/net-promoter-score.webp b/docs/images/xm-and-surveys/core-features/question-type/net-promoter-score.webp deleted file mode 100644 index 1b30bcc4d48a..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/net-promoter-score.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/ranking.webp b/docs/images/xm-and-surveys/core-features/question-type/ranking.webp deleted file mode 100644 index b057d34f3426..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/ranking.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/rating.webp b/docs/images/xm-and-surveys/core-features/question-type/rating.webp deleted file mode 100644 index 9b1000ba1e32..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/rating.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/schedule-a-meeting.webp b/docs/images/xm-and-surveys/core-features/question-type/schedule-a-meeting.webp deleted file mode 100644 index 55b291282cd2..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/schedule-a-meeting.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/select-single.webp b/docs/images/xm-and-surveys/core-features/question-type/select-single.webp deleted file mode 100644 index 8949f4ab5af0..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/select-single.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/question-type/statement-cta.webp b/docs/images/xm-and-surveys/core-features/question-type/statement-cta.webp deleted file mode 100644 index c0762f31985c..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/question-type/statement-cta.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/background-settings.webp b/docs/images/xm-and-surveys/core-features/styling-theme/background-settings.webp deleted file mode 100644 index 01fff26e7e38..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/background-settings.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/card-settings.webp b/docs/images/xm-and-surveys/core-features/styling-theme/card-settings.webp deleted file mode 100644 index 28fbade6c850..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/card-settings.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/doggo.webp b/docs/images/xm-and-surveys/core-features/styling-theme/doggo.webp deleted file mode 100644 index 65096ccd59d4..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/doggo.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/form-settings.webp b/docs/images/xm-and-surveys/core-features/styling-theme/form-settings.webp deleted file mode 100644 index 643cf950ac01..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/form-settings.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/hipster-living.webp b/docs/images/xm-and-surveys/core-features/styling-theme/hipster-living.webp deleted file mode 100644 index c83c00ed3ee3..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/hipster-living.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/look-and-feel.webp b/docs/images/xm-and-surveys/core-features/styling-theme/look-and-feel.webp deleted file mode 100644 index b7c8dcfff912..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/look-and-feel.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/mario.webp b/docs/images/xm-and-surveys/core-features/styling-theme/mario.webp deleted file mode 100644 index 62470994b789..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/mario.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/pre-requisite.webp b/docs/images/xm-and-surveys/core-features/styling-theme/pre-requisite.webp deleted file mode 100644 index 1b296658dfea..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/pre-requisite.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/step-eight.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-eight.webp deleted file mode 100644 index 5c35789d0390..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/step-eight.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/step-eleven.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-eleven.webp deleted file mode 100644 index 0cb0b13445aa..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/step-eleven.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/step-nine.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-nine.webp deleted file mode 100644 index bd8c73fe7e9e..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/step-nine.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/step-one.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-one.webp deleted file mode 100644 index d8908f7cd799..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/step-one.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/step-ten.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-ten.webp deleted file mode 100644 index 092b337eb7e8..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/step-ten.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/step-three.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-three.webp deleted file mode 100644 index cedee026fe5e..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/step-three.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/step-two.webp b/docs/images/xm-and-surveys/core-features/styling-theme/step-two.webp deleted file mode 100644 index 54f3d0e90710..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/step-two.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/styling-theme/windows-xp.webp b/docs/images/xm-and-surveys/core-features/styling-theme/windows-xp.webp deleted file mode 100644 index 45f588a28506..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/styling-theme/windows-xp.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/test-environment/modal.webp b/docs/images/xm-and-surveys/core-features/test-environment/modal.webp deleted file mode 100644 index e85bad83d78a..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/test-environment/modal.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/test-environment/more-actions.webp b/docs/images/xm-and-surveys/core-features/test-environment/more-actions.webp deleted file mode 100644 index 3bd4a4b14b75..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/test-environment/more-actions.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/core-features/validation-rules/editor.webp b/docs/images/xm-and-surveys/core-features/validation-rules/editor.webp deleted file mode 100644 index 80610ff4a20f..000000000000 Binary files a/docs/images/xm-and-surveys/core-features/validation-rules/editor.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/edit-multi-lang.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/edit-multi-lang.webp deleted file mode 100644 index ef987c81a527..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/edit-multi-lang.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/home-page.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/home-page.webp deleted file mode 100644 index 9f7392cd52ae..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/home-page.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/project-configuration.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/project-configuration.webp deleted file mode 100644 index 8ebe4d830057..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/project-configuration.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/see-survey-in-language.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/see-survey-in-language.webp deleted file mode 100644 index d117b3cc513d..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/see-survey-in-language.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languages-from-home.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languages-from-home.webp deleted file mode 100644 index c280254ad7d5..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languages-from-home.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languague-settings.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languague-settings.webp deleted file mode 100644 index c04301102838..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-languague-settings.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-sharing.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-sharing.webp deleted file mode 100644 index da6923586598..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/survey-sharing.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/translate-as-per-language.webp b/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/translate-as-per-language.webp deleted file mode 100644 index dc6b9b20c6cd..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/multi-language-surveys/translate-as-per-language.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/pre-requisite.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/pre-requisite.webp deleted file mode 100644 index 1b296658dfea..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/pre-requisite.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-eight.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-eight.webp deleted file mode 100644 index 5c35789d0390..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-eight.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-five.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-five.webp deleted file mode 100644 index 71daa7e7513f..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-five.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-four.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-four.webp deleted file mode 100644 index 3ca7dc38d9fe..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-four.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-one.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-one.webp deleted file mode 100644 index d8908f7cd799..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-one.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-seven.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-seven.webp deleted file mode 100644 index bcbabf831527..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-seven.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-six.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-six.webp deleted file mode 100644 index 3ac271b6c8d0..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-six.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-three.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-three.webp deleted file mode 100644 index cedee026fe5e..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-three.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-two.webp b/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-two.webp deleted file mode 100644 index 54f3d0e90710..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/overwrite-styling/step-two.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-one.webp b/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-one.webp deleted file mode 100644 index 1ceaa74231be..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-one.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-three.webp b/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-three.webp deleted file mode 100644 index 33f01ed624f8..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-three.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-two.webp b/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-two.webp deleted file mode 100644 index 4dcbe47395f0..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/schedule-start-end-dates/step-two.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/1-publish-to-web.webp b/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/1-publish-to-web.webp deleted file mode 100644 index 3d8996b62201..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/1-publish-to-web.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/2-warning-publish.webp b/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/2-warning-publish.webp deleted file mode 100644 index 0d8ac8585304..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/2-warning-publish.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/3-share-link.webp b/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/3-share-link.webp deleted file mode 100644 index 3ae5696f88e3..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/shareable-dashboards/3-share-link.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-one.webp b/docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-one.webp deleted file mode 100644 index 1a8587a024f7..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-one.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-two.webp b/docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-two.webp deleted file mode 100644 index b37dda4e3fd6..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/general-features/show-survey-to-percent-of-users/step-two.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/screening-out.webp b/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/screening-out.webp deleted file mode 100644 index 7d8dee37f290..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/link-surveys/market-research-panel/screening-out.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/surveys/link-surveys/source-tracking/share-link.webp b/docs/images/xm-and-surveys/surveys/link-surveys/source-tracking/share-link.webp deleted file mode 100644 index 12d9357727a9..000000000000 Binary files a/docs/images/xm-and-surveys/surveys/link-surveys/source-tracking/share-link.webp and /dev/null differ diff --git a/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/switch-to-dev.webp b/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/switch-to-dev.webp deleted file mode 100644 index 1fcb17b72a90..000000000000 Binary files a/docs/images/xm-and-surveys/xm/best-practices/docs-feedback/switch-to-dev.webp and /dev/null differ diff --git a/docs/platform/features/contacts.mdx b/docs/platform/features/contacts.mdx new file mode 100644 index 000000000000..94772881ca66 --- /dev/null +++ b/docs/platform/features/contacts.mdx @@ -0,0 +1,62 @@ +--- +title: "Contacts" +description: "Contacts are the people you survey. Store what you already know about them, then use it to decide who sees a survey and to read responses in context." +icon: "address-book" +--- + +A **contact** is one person in a workspace, with a set of attributes you supply. Contacts let you do two things you cannot do with anonymous responses: target a survey at a specific group, and see who said what. + +Contacts live under **Contacts** in the main navigation, which has three tabs — Contacts, Attributes and Segments. + + + Contacts are part of the Formbricks [Enterprise Edition](/self-hosting/advanced/license). + + +## Contacts + +The Contacts tab lists everyone in the workspace. + +![The Contacts tab, listing contacts with their default attributes](/images/platform/features/contacts/contacts-list.webp) + +Contacts arrive in three ways: + +- **From your app**, when the SDK identifies someone. See [User Identification](/surveys/website-app-surveys/user-identification) for `setUserId`, `setAttribute` and friends. +- **From a CSV**, using **Upload CSV**. Useful for a list you already hold outside your product. +- **From the API** — `POST /api/v2/management/contacts` for one, `PUT /api/v2/management/contacts/bulk` for many. Both need every attribute key to exist already. See the [API v2 reference](/api-v2-reference/introduction). + +Contacts are scoped to a **workspace**. A person known in one workspace is not automatically known in another. + +## Attributes + +An attribute is one field on a contact — a plan, a branch, a signup date. The Attributes tab lists every attribute key the workspace knows about. + +![The Attributes tab, listing every attribute key in the workspace](/images/platform/features/contacts/attribute-keys.webp) + +Two kinds: + +- **Default attributes** — `email`, `userId`, `firstName`, `lastName`. Formbricks creates these itself and they cannot be deleted, which is why they have no checkbox in the table. +- **Custom attributes** — anything you send. Create them ahead of time with **Create attribute**, or let them appear the first time the SDK sends one. + +Each key carries a label, the key your code uses, and a data type. + + + A workspace can hold up to **150 attribute keys**. That is a limit on the number of distinct fields, not on how many contacts have them. + + +## Segments + +A segment is a saved group of contacts, defined by a filter rather than a fixed list. Membership is evaluated when it is used, so a contact joins or leaves as their attributes change. + +![The Segments tab, showing a saved segment](/images/platform/features/contacts/segments.webp) + +Filters combine attributes, devices and previous survey interaction, and can be grouped with **and** / **or**. A segment built on "plan equals premium" always means today's premium customers, without anyone maintaining a list. + +Segments are what [attribute-based targeting](/surveys/website-app-surveys/attribute-based-targeting) uses to decide who sees an app survey. + +## What Contacts change about a response + +Once a response is linked to a contact, it stops being anonymous: + +- The response table shows who answered instead of "Anonymous". +- You can filter and segment responses by any attribute the contact carries. +- [Personal Links](/surveys/link-surveys/personal-links) can be generated per contact, so a link survey attributes its answers too. diff --git a/docs/platform/features/integrations/hubspot.mdx b/docs/platform/features/integrations/hubspot.mdx index a2d1b9cb15d7..4b23593ea3a3 100644 --- a/docs/platform/features/integrations/hubspot.mdx +++ b/docs/platform/features/integrations/hubspot.mdx @@ -138,7 +138,7 @@ For maximum flexibility, you can use Formbricks webhooks with a custom endpoint - Go to **Configuration** → **Integrations** in Formbricks, click **Manage Webhooks** → **Add Webhook**, enter your endpoint URL, select **Response Finished** as the trigger, and choose the surveys to monitor. + Go to **Settings → Workspace → Integrations** in Formbricks, click **Manage Webhooks** → **Add Webhook**, enter your endpoint URL, select **Response Finished** as the trigger, and choose the surveys to monitor. ![Integrations Tab](/images/platform/features/integrations/webhooks/integrations-tab.webp) diff --git a/docs/platform/features/integrations/n8n.mdx b/docs/platform/features/integrations/n8n.mdx index 2eb92f9cc90c..f81801d8ee7e 100644 --- a/docs/platform/features/integrations/n8n.mdx +++ b/docs/platform/features/integrations/n8n.mdx @@ -64,7 +64,7 @@ Here, we are adding `Response Finished` as an event, which will trigger when the ## Step 5: Select Survey -Next, you can choose from all the surveys you have created in this environment. You can select multiple surveys: +Next, you can choose from all the surveys you have created in this workspace. You can select multiple surveys: ![Select Survey](/images/platform/features/integrations/n8n/select-survey.webp) diff --git a/docs/platform/features/integrations/overview.mdx b/docs/platform/features/integrations/overview.mdx index 6aca05e20214..f393465160a9 100644 --- a/docs/platform/features/integrations/overview.mdx +++ b/docs/platform/features/integrations/overview.mdx @@ -29,6 +29,8 @@ At Formbricks, we understand the importance of integrating with third-party appl * [Slack](/platform/features/integrations/slack): Automatically send responses to a Slack channel of your choice on response events. +* [Webhooks](/platform/features/integrations/webhooks): Send real-time HTTP notifications to any endpoint of your choice when responses come in. + * [Wordpress](/platform/features/integrations/wordpress)(Open Source): Automatically integrate your Formbricks surveys with your Wordpress website. * [Zapier](/platform/features/integrations/zapier): Connect Formbricks with 2000+ apps on Zapier. diff --git a/docs/platform/features/integrations/zapier.mdx b/docs/platform/features/integrations/zapier.mdx index b85ba0971c7b..505beaff9a38 100644 --- a/docs/platform/features/integrations/zapier.mdx +++ b/docs/platform/features/integrations/zapier.mdx @@ -54,7 +54,7 @@ Once you copied it in the newly opened Zapier window, you will be connected: ## Step 5: Select Survey -Next, you can choose from all the surveys you have created in this environment: +Next, you can choose from all the surveys you have created in this workspace: ![Select Survey](/images/platform/features/integrations/zapier/select-survey.webp) diff --git a/docs/platform/features/styling-theme.mdx b/docs/platform/features/styling-theme.mdx index b2f48c96c425..f11572eb7ae0 100644 --- a/docs/platform/features/styling-theme.mdx +++ b/docs/platform/features/styling-theme.mdx @@ -16,7 +16,7 @@ Keep the survey styling consistent over all surveys with a Styling Theme. Custom In the left sidebar, open **Settings → Workspace → Appearance**: -![Appearance](/images/platform/features/styling-theme/form-css-styling.webp) +![The Appearance settings, with the theme editor and a live preview](/images/platform/features/styling-theme/appearance.webp) ## Survey styling @@ -125,31 +125,12 @@ Customize your survey with your brand's logo. Brand logos are only visible on Link Survey pages. - - - In **Settings → Workspace → Appearance**, scroll down to the **Logo Upload** box. +1. In **Settings → Workspace → Appearance**, scroll down to **Logo**. +2. Upload your logo. Logos must be 5 MB or less. +3. Optionally turn on **Add background color** — useful for a transparent logo that needs a solid backdrop. +4. Choose **Save**. The logo only takes effect once saved. - ![Logo upload box](/images/platform/features/styling-theme/step-four.webp) - - - - Upload your logo. Logos must be 5 MB or less. - - ![Upload logo](/images/platform/features/styling-theme/step-five.webp) - - - - If you've uploaded a transparent image and want to add a background to it, enable the toggle and select a color. - - ![Logo background color](/images/platform/features/styling-theme/step-six.webp) - - - - Remember to save your changes! - - ![Save changes](/images/platform/features/styling-theme/step-seven.webp) - - +![The Logo section with a logo uploaded](/images/platform/features/styling-theme/logo.webp) The logo settings apply across all Link Survey pages. @@ -157,7 +138,7 @@ Customize your survey with your brand's logo. You can allow overwriting the styling theme for individual surveys to create unique styles per survey: -![Allow overwrite toggle](/images/platform/features/styling-theme/allow-overwrite.webp) +![The Enable custom styling toggle](/images/platform/features/styling-theme/enable-custom-styling.webp) In the survey editor, a **Styling** tab will appear where you can overwrite the default styling theme. See the [Custom Styling](/surveys/general-features/overwrite-styling) guide for details. diff --git a/docs/platform/features/user-management/organizations-and-roles.mdx b/docs/platform/features/user-management/organizations-and-roles.mdx index 372898a15418..e47264eedb67 100644 --- a/docs/platform/features/user-management/organizations-and-roles.mdx +++ b/docs/platform/features/user-management/organizations-and-roles.mdx @@ -38,7 +38,11 @@ To prevent privilege escalation, the following rules apply: ## Organization-level roles -All users and their organization-level roles are listed under **Settings → Organization → Teams**. Users can hold any of the following org-level roles: +All users and their organization-level roles are listed under **Settings → Organization → Teams**. The role dropdown beside each person is where you change one. + +![The organization's member list, with a role beside each person](/images/platform/features/user-management/organizations-and-roles/members.webp) + +Users can hold any of the following org-level roles: ### Owner - Have full access to the organization, its data, and settings diff --git a/docs/platform/features/user-management/teams-and-roles.mdx b/docs/platform/features/user-management/teams-and-roles.mdx index 776514448ce3..0ad97399833d 100644 --- a/docs/platform/features/user-management/teams-and-roles.mdx +++ b/docs/platform/features/user-management/teams-and-roles.mdx @@ -23,6 +23,10 @@ Formbricks uses a two-tier permission system: - Team-level roles provide granular control for specific teams - Workspace permissions further refine what users can do within individual workspaces +Teams live under **Settings → Organization → Teams**, below the member list. + +![The Teams card, listing each team and its size](/images/platform/features/user-management/teams-and-roles/teams.webp) + ## Team-level roles ### Team Admins @@ -39,6 +43,10 @@ Formbricks uses a two-tier permission system: ## Workspace-level permissions +A team is granted access to a workspace one workspace at a time, under **Settings → Workspace → Team Access**. The permission there decides what the team's members can do in that workspace. + +![Team Access, showing each team's permission on this workspace](/images/platform/features/user-management/teams-and-roles/workspace-access.webp) + Within each workspace, team members can have one of three permission levels: ### Read diff --git a/docs/platform/features/user-management/two-factor-auth.mdx b/docs/platform/features/user-management/two-factor-auth.mdx index 55c28282fad0..62c7b5e32461 100644 --- a/docs/platform/features/user-management/two-factor-auth.mdx +++ b/docs/platform/features/user-management/two-factor-auth.mdx @@ -24,6 +24,9 @@ Users can enable 2FA from their profile: 1. Navigate to **Settings → Account → Your Profile** via the menu in the lower right corner 2. In the **Security** section, toggle the **Two-factor authentication** switch + +![The Security section of your profile settings](/images/platform/features/user-management/two-factor-auth/setup.webp) + 3. Follow the setup wizard: **Step 1: Confirm Password** diff --git a/docs/platform/introduction.mdx b/docs/platform/introduction.mdx index 1b7a6b8fb85c..e5253a21861a 100644 --- a/docs/platform/introduction.mdx +++ b/docs/platform/introduction.mdx @@ -15,18 +15,27 @@ This guide covers everything you need to set up, use, and develop with Formbrick - - Learn how to use Formbricks' XM & Surveys to collect feedback from your customers, users, and employees. + + Collect feedback from customers, users, and employees with link, website, and in-app surveys. - - Learn how to self-host Formbricks on your infrastructure. - - - Learn how to use Formbricks' API to CRUD various resources programmatically. - + + Bring feedback from every source into one store and turn it into insights. + + + + Automate what happens next with triggers, filters, actions, and observable runs. + + + + Learn how to self-host Formbricks on your infrastructure. + + + + Learn how to use Formbricks' API to CRUD various resources programmatically. + - + Warm up with the Formbricks code base to make changes to the platform. diff --git a/docs/platform/what-is-formbricks.mdx b/docs/platform/what-is-formbricks.mdx index 24f0b0efe0a2..4b84de506ae5 100644 --- a/docs/platform/what-is-formbricks.mdx +++ b/docs/platform/what-is-formbricks.mdx @@ -28,7 +28,7 @@ Formbricks covers the full experience-management loop — from collecting feedba Unify feedback from every source into one store, then visualize it with charts and shareable dashboards. - Turn responses into action with Workflows that trigger emails and other automations — no code required. + Turn responses into action with Workflows that send an email when a response comes in — no code required. diff --git a/docs/self-hosting/advanced/enterprise-features/workflows.mdx b/docs/self-hosting/advanced/enterprise-features/workflows.mdx index 76090ce5bbba..841cba33a63b 100644 --- a/docs/self-hosting/advanced/enterprise-features/workflows.mdx +++ b/docs/self-hosting/advanced/enterprise-features/workflows.mdx @@ -5,6 +5,6 @@ icon: "https://d3gk2c5xim1je2.cloudfront.net/lucide/v1.16.0/workflow.svg" sidebarTitle: "Workflows" --- -Workflows automate tasks in response to events in Formbricks. You choose a trigger, narrow down which events qualify with optional filters, and run an action such as sending an email. +Workflows automate tasks in response to events in Formbricks. You choose a trigger, narrow down which events qualify with optional filters, and run an action. Today that action is sending an email. Read the full guide: [Workflows overview](/workflows/overview). diff --git a/docs/self-hosting/advanced/migration.mdx b/docs/self-hosting/advanced/migration.mdx index ad5c44255992..a1895377ce07 100644 --- a/docs/self-hosting/advanced/migration.mdx +++ b/docs/self-hosting/advanced/migration.mdx @@ -201,7 +201,7 @@ HAVING COUNT(w."workspaceId") > 1; Most instances return no rows: a workspace is normally assigned a single dataset, so sharing only exists where an administrator deliberately assigned a second workspace. For any row that does come back, an integration that mutates records in that dataset has two ways forward — assign the dataset to a single -workspace under **Settings → Organization → Feedback Datasets**, or have the integration authenticate as +workspace under **Settings → Organization → Datasets**, or have the integration authenticate as an organization owner or manager instead of with a workspace-scoped key. #### What to do before upgrading diff --git a/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx b/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx index c87182f8fda7..caa625bf4e72 100644 --- a/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx +++ b/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx @@ -23,6 +23,15 @@ Integrating your own OIDC (OpenID Connect) instance with your Formbricks instanc `{WEBAPP_URL}/api/auth/oauth2/callback/openid`. + + **Using Microsoft Entra ID?** Configure it through the dedicated [Azure AD + provider](/self-hosting/configuration/auth-sso/azure-ad-oauth) rather than here — it maps Entra's + profile correctly. If you do point `OIDC_ISSUER` at a multi-tenant authority + (`login.microsoftonline.com/common` or `/organizations`), Formbricks skips OpenID discovery for it + and logs a warning at startup: those authorities advertise a placeholder issuer instead of a real + one, so id_tokens cannot be verified against it and sign-in would otherwise fail. + + **Upgrading from v5.1 or earlier?** This path changed once, at v5.2: diff --git a/docs/self-hosting/configuration/auth-sso/saml-sso.mdx b/docs/self-hosting/configuration/auth-sso/saml-sso.mdx index d7d2c0dab500..0bccde23b6dd 100644 --- a/docs/self-hosting/configuration/auth-sso/saml-sso.mdx +++ b/docs/self-hosting/configuration/auth-sso/saml-sso.mdx @@ -73,7 +73,7 @@ To configure SAML SSO in Formbricks, follow these steps: - Create a SAML application in your IdP by following your provider's instructions([SAML Setup](/development/guides/auth-and-provision/setup-saml-with-identity-providers)) + Create a SAML application in your IdP by following your provider's instructions. See [SAML with Identity Providers](/self-hosting/configuration/auth-sso/setup-saml-with-identity-providers) for the values Formbricks expects. diff --git a/docs/development/guides/auth-and-provision/setup-saml-with-identity-providers.mdx b/docs/self-hosting/configuration/auth-sso/setup-saml-with-identity-providers.mdx similarity index 78% rename from docs/development/guides/auth-and-provision/setup-saml-with-identity-providers.mdx rename to docs/self-hosting/configuration/auth-sso/setup-saml-with-identity-providers.mdx index 2ee97ab1e86b..99a117bd9a8c 100644 --- a/docs/development/guides/auth-and-provision/setup-saml-with-identity-providers.mdx +++ b/docs/self-hosting/configuration/auth-sso/setup-saml-with-identity-providers.mdx @@ -1,9 +1,10 @@ --- -title: "Setup SAML with Identity Providers" +title: "SAML with Identity Providers" description: "This guide explains the settings you need to use to configure SAML with your Identity Provider. Once configured, obtain an XML metadata file and use it to configure SAML in Formbricks." +icon: "building-lock" --- -### SAML Registration with Identity Providers +## SAML registration with identity providers This guide explains the settings you need to use to configure SAML with your Identity Provider. Once configured, obtain an XML metadata file and use it to configure SAML in Formbricks. @@ -59,52 +60,52 @@ This guide explains the settings you need to use to configure SAML with your Ide Above provided claims may differ based on your configuration and the IdP you are using. Please refer to the documentation of your IdP for the correct claims. -### SAML With Okta +## SAML with Okta For example, in Okta, once you create an account, you can click on Applications on the sidebar menu: - + - + - + - + - **Single Sign-On URL**: `https:///api/auth/saml/callback` or `http://localhost:3000/api/auth/saml/callback` (if you are running Formbricks locally) - **Audience URI (SP Entity ID)**: `https://saml.formbricks.com` (hardcoded; do not replace with your instance URL) - + - + - + - + - + - + - + - + - + diff --git a/docs/self-hosting/configuration/environment-variables.mdx b/docs/self-hosting/configuration/environment-variables.mdx index 4164006867f5..d6d16920a480 100644 --- a/docs/self-hosting/configuration/environment-variables.mdx +++ b/docs/self-hosting/configuration/environment-variables.mdx @@ -48,6 +48,7 @@ For `AI_PROVIDER=openai-compatible`, the LLM GA v1 self-hosted path is Qwen serv | PASSWORD_RESET_TOKEN_LIFETIME_MINUTES | Configures how long password reset links remain valid in minutes. Accepted values are integers from 5 to 120. | optional | 30 | | EMAIL_VERIFICATION_DISABLED | Disables email verification if set to 1. | optional | | | RATE_LIMITING_DISABLED | Disables only the application-level rate limiter if set to 1. It does not disable Envoy or an equivalent edge rate limiter. | optional | | +| ENTERPRISE_LICENSE_KEY | License key for the Formbricks Enterprise Edition. Unlocks Enterprise-only features; the instance validates it against the Formbricks license server, so it needs outbound network access (or `HTTP_PROXY`/`HTTPS_PROXY`). See [License Activation](/self-hosting/advanced/license-activation). | optional (required for Enterprise Edition features) | | | TELEMETRY_DISABLED | Disables telemetry reporting if set to 1. Ignored when an Enterprise License is active. | optional | | | DANGEROUSLY_ALLOW_WEBHOOK_INTERNAL_URLS | Allows webhook URLs to point to internal/private network addresses (e.g. localhost, 192.168.x.x) if set to 1. Useful for self-hosted instances that need to send webhooks to internal services. | optional | | | INVITE_DISABLED | Disables the ability for invited users to create an account if set to 1. | optional | | @@ -91,6 +92,7 @@ For `AI_PROVIDER=openai-compatible`, the LLM GA v1 self-hosted path is Qwen serv | STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | | | STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | | | DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b | +| UNSPLASH_ACCESS_KEY | Unsplash access key used to search Unsplash for survey background images. Without it, the image tab is hidden in the survey editor's background picker and in the Look & Feel settings. | optional (required for Unsplash background images) | | | DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | | | OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | | | OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | | diff --git a/docs/self-hosting/configuration/integrations.mdx b/docs/self-hosting/configuration/integrations.mdx new file mode 100644 index 000000000000..0e444766d965 --- /dev/null +++ b/docs/self-hosting/configuration/integrations.mdx @@ -0,0 +1,96 @@ +--- +title: "Third-party Integrations (On Premise)" +sidebarTitle: "Overview" +description: "Configure third-party integrations on a self-hosted Formbricks instance." +icon: "bridge" +--- + +Formbricks connects to third-party tools such as Slack, Notion, or Google Sheets on behalf of your users. On +Formbricks Cloud we register and operate the OAuth apps behind those connections, so they work out of the +box. A self-hosted instance talks to those services under your own identity instead, which means you +register the OAuth app yourself and hand its credentials to your instance. + + + Using Formbricks Cloud? You do not need any of this. Head to the [Cloud integration + guides](/platform/features/integrations/overview) instead. + + +## Before you configure an integration + + + + Every OAuth app needs a redirect URL that points back at your instance, built from your own + `https://` rather than `app.formbricks.com`. The callback path differs per + service, so take it from the guide you are following. + + + + Slack refuses to talk to an instance without a valid certificate, and the other providers expect HTTPS in + production too. See [Custom SSL](/self-hosting/configuration/custom-ssl) if you have not set this up yet. + + + + Credentials go into the same place as every other setting — see [Environment + Variables](/self-hosting/configuration/environment-variables). Restart your containers for new values to + take effect. + + + +## What each integration needs + +The integrations fall into two groups. The first four need credentials from an OAuth app you register with +the third-party service. The last three connect from the other side — through a Formbricks API key or your +instance URL — and need no server configuration at all. + +| Integration | What you register | Environment variables | +| ----------------------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| [Airtable](/self-hosting/configuration/integrations/airtable) | An OAuth integration in the Airtable Developer hub | `AIRTABLE_CLIENT_ID` | +| [Google Sheets](/self-hosting/configuration/integrations/google-sheets) | An OAuth client in a Google Cloud project | `GOOGLE_SHEETS_CLIENT_ID`, `GOOGLE_SHEETS_CLIENT_SECRET`, `GOOGLE_SHEETS_REDIRECT_URL` | +| [Notion](/self-hosting/configuration/integrations/notion) | A public Notion integration | `NOTION_OAUTH_CLIENT_ID`, `NOTION_OAUTH_CLIENT_SECRET` | +| [Slack](/self-hosting/configuration/integrations/slack) | A Slack app with bot scopes and public distribution | `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET` | +| [n8n](/self-hosting/configuration/integrations/n8n) | Nothing — n8n authenticates with a Formbricks API key | None | +| [Zapier](/self-hosting/configuration/integrations/zapier) | Nothing — Zapier authenticates with a Formbricks API key | None | +| [ActivePieces](/self-hosting/configuration/integrations/activepieces) | Nothing — point the connection at your instance URL | None | + +## Configuration guides + + + + Send responses to an Airtable base. + + + + Send responses to a Google Sheet. + + + + Send responses to a Notion database. + + + + Post responses to a Slack channel. + + + + Build automations on survey events with n8n. + + + + Connect Formbricks to thousands of apps on Zapier. + + + + Build automations on survey events with ActivePieces. + + + +Once an integration is enabled on your instance, connecting it to a survey works exactly as it does on +Formbricks Cloud. Follow the matching [Cloud guide](/platform/features/integrations/overview) from there. + + + For automations that stay inside Formbricks — without a third-party tool — use + [Workflows](/workflows/overview) to trigger actions such as sending an email when a response is completed. + + +Still struggling or something not working as expected? [Join our GitHub +Discussions](https://github.com/formbricks/formbricks/discussions) and we'd be glad to assist you! diff --git a/docs/self-hosting/configuration/integrations/n8n.mdx b/docs/self-hosting/configuration/integrations/n8n.mdx index 66cb7d737639..df65097a8c2c 100644 --- a/docs/self-hosting/configuration/integrations/n8n.mdx +++ b/docs/self-hosting/configuration/integrations/n8n.mdx @@ -40,7 +40,7 @@ Once you copied it in the API Key field, hit Save button to test the connection Here, we are adding `Response Finished` as an event, which will trigger when the survey has been filled out. -* Select Survey: Next, you can choose from all the surveys you have created in this environment. You can select multiple surveys: +* Select Survey: Next, you can choose from all the surveys you have created in this workspace. You can select multiple surveys: ![select survey](https://res.cloudinary.com/dwdb9tvii/image/upload/v1738253219/image_hbubu7.jpg) diff --git a/docs/surveys/analysis/reading-results.mdx b/docs/surveys/analysis/reading-results.mdx new file mode 100644 index 000000000000..8df4604a394b --- /dev/null +++ b/docs/surveys/analysis/reading-results.mdx @@ -0,0 +1,54 @@ +--- +title: "Reading Survey Results" +description: "Every published survey has two views: a Summary that aggregates answers question by question, and a Responses table with one row per submission." +icon: "chart-simple" +--- + +Open any survey and you land on its results. There are two tabs, and they answer different questions. + +- **Summary** — what did people say, in aggregate? +- **Responses** — what did *this* person say? + +## Summary + +The Summary tab opens with five figures for the survey as a whole, then a card per question. + +![The survey summary, with completion stats and per-question breakdowns](/images/surveys/analysis/summary.webp) + +The headline figures: + +| Figure | What it counts | +| --- | --- | +| **Impressions** | How many times the survey was displayed. | +| **Starts** | How many people began answering, with the share of impressions. | +| **Completed** | How many reached the end. | +| **Drop-Offs** | How many started but did not finish, with the share of starts. | +| **Time to Complete** | How long a completed response takes, once there are enough to average. | + +Below them, each question gets its own card in survey order. What the card shows depends on the question: open text lists individual answers, choice questions show each option with its share, and rating-style questions show the distribution. + +## Responses + +The Responses tab is one row per submission, with a column per question. + +![The response table, one row per submission](/images/surveys/analysis/responses.webp) + +The table also carries columns you did not ask for directly: + +- **Status** — whether the response was completed or abandoned part-way. +- **Person** — the [contact](/platform/features/contacts) who answered, or *Anonymous*. +- **Tags** — any [tags](/surveys/general-features/tags) applied to the response. +- **[Hidden fields](/surveys/general-features/hidden-fields)** — values passed in with the survey rather than answered. +- **[Metadata](/surveys/general-features/metadata)** — source, URL, browser, OS, device and country. + +The table is wide, so scroll horizontally to reach the later columns, or use the column settings to hide the ones you do not need. + +## Filtering and exporting + +Both tabs share the same **Filter** and time-range controls at the top, so a filter you set on the Summary carries to the Responses table. Filter on any answer, tag, hidden field or metadata value. + +**Download** offers three scopes, each as CSV or Excel: + +- **All responses** — everything, ignoring the current filter. +- **Filtered responses** — only what the current filter matches. +- **Selected responses** — only the rows you ticked in the table. diff --git a/docs/surveys/general-features/add-image-or-video-question.mdx b/docs/surveys/general-features/add-image-or-video-question.mdx index 2af259412d58..f8bfdae185b4 100644 --- a/docs/surveys/general-features/add-image-or-video-question.mdx +++ b/docs/surveys/general-features/add-image-or-video-question.mdx @@ -10,21 +10,19 @@ icon: "image" uploads](/self-hosting/configuration/file-uploads) before using this feature. -## How to Add Images +## How to add images -Click the icon on the right side of the question to add an image or video: +Click the image icon to the right of the question field. A panel opens above it with an **Image** and a **Video** tab. -![Access Question settings](/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question.webp) +![The media panel on its Image tab](/images/surveys/general-features/add-image-or-video-question/media-panel.webp) -Upload an image by clicking the upload icon or dragging the file. Images must be 5 MB or less: +On the **Image** tab, upload a file by clicking the box or dragging onto it. Images must be 5 MB or less. -![Overview of adding image to question](/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-image.webp) +## How to add videos -## How to Add Videos +Switch to the **Video** tab and paste a link. -Toggle to add a video via link: - -![Add a video to question](/images/surveys/general-features/add-image-or-video-question/add-image-or-video-to-question-video.webp) +![The media panel on its Video tab](/images/surveys/general-features/add-image-or-video-question/media-video.webp) ### Supported Video Platforms diff --git a/docs/surveys/general-features/conditional-logic.mdx b/docs/surveys/general-features/conditional-logic.mdx index 62f6d79966e7..2a16e79e8844 100644 --- a/docs/surveys/general-features/conditional-logic.mdx +++ b/docs/surveys/general-features/conditional-logic.mdx @@ -1,100 +1,71 @@ --- title: "Conditional Logic" -description: "Create complex survey logic with the Logic Editor. Use conditions, actions, and variables to create a personalized survey experience." +description: "Send different respondents down different paths. Conditional logic reads answers as they come in and decides what happens next." icon: "code-branch" --- +Conditional logic is a set of rules attached to a **block**. Each rule reads *when this is true, then do that* — skip ahead, require an answer, or update a variable. -![Add conditions](/images/surveys/general-features/conditional-logic/editor.webp) +## Where logic lives -## Terminology - -* **Condition**: A rule that determines when an action should be executed. - -* **Action**: A task that is executed when a condition is met. +Logic belongs to the block, not to a single question. Scroll to the foot of any block in the survey editor and you will find **Conditional Logic** below the question list. -## **Creating Logic** +![The Conditional Logic section at the foot of a block](/images/surveys/general-features/conditional-logic/add-logic.webp) -* **Add a Logic Block**: Click the `Add logic +` button to add a new logic block. - -![Add conditions](/images/surveys/general-features/conditional-logic/add-logic.webp) +## Terminology - - You can add multiple logic blocks to a survey. Logic blocks are executed in - the order they are added. You can rearrange the order of logic blocks. - +* **Condition**: a rule that decides whether an action runs. -* **Add Conditions**: Add conditions to the logic block. Conditions are rules that determine when an action should be executed. +* **Action**: what happens when the condition is met. -![Add conditions](/images/surveys/general-features/conditional-logic/conditions.webp) +## Building a rule -Conditons can be based on: +Click `Add logic +` and fill in the sentence. -* **Question**: The answer to a question. +![A rule that routes savings-account respondents to a second block](/images/surveys/general-features/conditional-logic/logic-rule.webp) -* **Variable**: A variable value. + + + The **When** row starts with a source. It can be the answer to a **question**, the value of a **variable**, or the value of a **hidden field**. + -* **Hidden Field**: The value of a hidden field.2.a **Condition Options**: Choose from a list of available conditions. + + The operators on offer depend on the source. A single-select question can be compared with `Equals`, `Does not equal`, `Equals one of` and `Is submitted`; a free text question adds `Contains`, `Starts with`, `Ends with` and their negations. -![Condition Options](/images/surveys/general-features/conditional-logic/condition-options.webp) + ![The operator list for a single-select question](/images/surveys/general-features/conditional-logic/condition-operators.webp) + -* **Condition Operators**: Choose an operator to compare the condition value. + + Compare against a fixed value, or against another question's answer, a variable, or a hidden field. + -![Condition Operators](/images/surveys/general-features/conditional-logic/condition-operators.webp) + + The **Then** row is what happens when the condition holds. -* **Condition Value**: Enter a value to compare the condition against. - Comparisons can be made against a fixed value or a dynamic value. - Dynamic values can be based on a question, variable, or hidden field. + ![The three actions: Calculate, Require Answer and Jump to block](/images/surveys/general-features/conditional-logic/action-options.webp) + + -![Condition Value](/images/surveys/general-features/conditional-logic/condition-value.webp) +## Actions - - Conditions can be grouped. Conditions can be combined using AND or OR - operators. You can add multiple conditions to a logic block. Conditions are - evaluated in the order they are added. - +* **Calculate**: change a variable's value. The result is available to every later question, and to the rules below this one. -![Condition Chaining](/images/surveys/general-features/conditional-logic/condition-chaining.webp) +* **Require Answer**: make an optional question required. Only questions that are optional to begin with can be required this way. -* **Add Actions**: Add actions to the logic block. Actions are tasks that are executed when a condition is met. +* **Jump to block**: send the respondent to a specific block instead of the next one. - You can add multiple actions to a logic block. Actions are executed in the - order they are added. + A rule can carry several actions. They run in the order they are listed. -* **Action Options**: Choose from a list of available actions. - -![Add Actions](/images/surveys/general-features/conditional-logic/action-options.webp) - -Action is of the following types: - - * **Calculate**: Perform a calculation. These variables are then available for use in other questions. - - * Calculations can be performed on variables. - - * Calculations can be based on fixed values or dynamic values. - - ![Action Calculate Variables](/images/surveys/general-features/conditional-logic/action-calculate-variables.webp) - - ![Action Calculate Operators](/images/surveys/general-features/conditional-logic/action-calculate-operators.webp) - - ![Action Calculate value](/images/surveys/general-features/conditional-logic/action-calculate-value.webp) - - ![Action Calculate](/images/surveys/general-features/conditional-logic/action-calculate.webp) - - * **Require Answer**: Make a question required. Only the optional questions can be marked as required while filling the survey. - - ![Action Require](/images/surveys/general-features/conditional-logic/action-require.webp) +## Everyone else - * **Jump to Block**: Skip to a specific block. The user will be redirected to the specified block based on the condition. - - ![Action Jump](/images/surveys/general-features/conditional-logic/action-jump.webp) +Under the rules sits **All other answers will continue to**. This is the fallback for respondents who match none of the conditions, and it is set to the next block unless you change it. -* **Save Logic**: Click the `Save` button to save the logic block. +## Several conditions in one rule -## Block Logic +The `⋮` button beside a condition adds another condition below it, duplicates it, removes it, or wraps it in a **group**. Conditions are joined with `and` or `or`, and a group is how you mix the two without ambiguity — the same reason brackets exist in arithmetic. -This logic is executed when the user reaches the block. Logic can be as simple as showing a follow-up block based on earlier answers or as complex as calculating a score based on multiple answers. +## Several rules in one block -![Block Logic](/images/surveys/general-features/conditional-logic/question-logic.webp) \ No newline at end of file +`Add logic +` adds another rule. Rules are evaluated top to bottom, and the first one whose condition holds decides where the respondent goes. Drag them to change the order. diff --git a/docs/surveys/general-features/email-followups.mdx b/docs/surveys/general-features/email-followups.mdx index 4d032859389a..06b5566c0fba 100644 --- a/docs/surveys/general-features/email-followups.mdx +++ b/docs/surveys/general-features/email-followups.mdx @@ -5,9 +5,9 @@ icon: "envelope" --- - Email Follow-ups are being deprecated in favor of [Workflows](/workflows/overview). Workflows - offer the same response- and ending-based email automation plus additional triggers and actions. We recommend - building new email automations as Workflows. + Email Follow-ups are being deprecated in favor of [Workflows](/workflows/overview). Workflows cover the + same ground — a completed response, optionally narrowed to specific ending cards, sending an email — and + are where this is being developed. We recommend building new email automations as Workflows. @@ -35,8 +35,10 @@ Email followups allow you to automatically send customized emails to respondents ## Setting Up Email Follow-ups - - Navigate to the survey editor and access the Follow-ups section. + + It sits at the end of the survey editor's tab row, after Settings. `New follow-up` opens the dialog where the rest of these steps happen. + + The Follow-ups tab of the survey editor @@ -51,7 +53,7 @@ Email followups allow you to automatically send customized emails to respondents
  • Yourself: Your own email address
  • - Followup recipient configuration + The Create a new follow-up dialog
    @@ -61,8 +63,6 @@ Email followups allow you to automatically send customized emails to respondents - Followup content configuration -
    • Subject: Customize your email subject line
    • Body: Supports basic HTML formatting (`p`, `span`, `b`, `strong`, `i`, `em`, `a`, `br` tags)
    • diff --git a/docs/surveys/general-features/hidden-fields.mdx b/docs/surveys/general-features/hidden-fields.mdx index 5066fb727362..805408952203 100644 --- a/docs/surveys/general-features/hidden-fields.mdx +++ b/docs/surveys/general-features/hidden-fields.mdx @@ -6,19 +6,10 @@ icon: "eye-slash" ## How to Add Hidden Fields -### Enable Hidden Fields +1. Open the survey in the editor, stay on the **Questions** tab and scroll to the bottom. You will find a **Hidden fields** section. +2. Type a field ID and choose **Add hidden field ID**. Add as many as you need. -1. Edit the survey you want to add hidden fields to & switch to the Questions tab and scroll down to the bottom of the page. You will see a section called **Hidden Fields**. Make sure to enable it by toggling the switch. - -![Enable Hidden Fields](/images/surveys/general-features/hidden-fields/hidden-fields.webp) - -### Add Hidden Field IDs - -1. Now click on it to add a new hidden field ID. You can add as many hidden fields as you want. - -![Add Hidden Fields](/images/surveys/general-features/hidden-fields/input-hidden-fields.webp) - -![Filled Hidden Fields](/images/surveys/general-features/hidden-fields/filled-hidden-fields.webp) +![Hidden fields in the survey editor](/images/surveys/general-features/hidden-fields/editor.webp) ## Set Hidden Field via URL @@ -49,7 +40,7 @@ formbricks.track("action_name", {hiddenFields: {myField: "value"}}) These hidden fields will now be visible in the responses tab just like other fields in the Summary as well as the Response Cards, and you can use them to filter and analyze your responses. -![Hidden Field Responses](/images/surveys/general-features/hidden-fields/hidden-field-responses.webp) +![Hidden field values in the response table](/images/surveys/general-features/hidden-fields/responses.webp) ## Use Cases diff --git a/docs/surveys/general-features/hide-back-button.mdx b/docs/surveys/general-features/hide-back-button.mdx index 1ee08626087c..43bee27f45b4 100644 --- a/docs/surveys/general-features/hide-back-button.mdx +++ b/docs/surveys/general-features/hide-back-button.mdx @@ -8,4 +8,4 @@ Surveys display a back button by default. If you want to prevent respondents fro To disable the back button, navigate to the survey settings and select the Response options tab. -![Hide back button](/images/surveys/general-features/hide-back-button/hide-back-button.webp) +![Hide back button](/images/surveys/general-features/hide-back-button/response-option.webp) diff --git a/docs/surveys/general-features/limit-submissions.mdx b/docs/surveys/general-features/limit-submissions.mdx index 93a69818ec8a..fa2d1c0e2c6a 100644 --- a/docs/surveys/general-features/limit-submissions.mdx +++ b/docs/surveys/general-features/limit-submissions.mdx @@ -6,7 +6,7 @@ icon: "lock" - **How to**: Open the Survey Editor, switch to the Settings tab. Scroll down to **Response Options**, and toggle the **“Close survey on response limit”**. -![Limit Submissions](/images/surveys/general-features/limit-submissions/limit-submissions.webp) +![Limit Submissions](/images/surveys/general-features/limit-submissions/response-option.webp) - **Details**: Set a specific number of responses after which the survey automatically closes. diff --git a/docs/surveys/general-features/metadata.mdx b/docs/surveys/general-features/metadata.mdx index 771f4c7ed788..3a8eef615226 100644 --- a/docs/surveys/general-features/metadata.mdx +++ b/docs/surveys/general-features/metadata.mdx @@ -8,7 +8,7 @@ icon: "user" ## Metadata Captured -- **Source**: The source from where the user filled the survey. This could be an App, Link, or a Webpage. +- **Source**: Where the respondent came from. A link survey opened with `?source=` records that value; without one it records the survey's type, `link` or `app`. See [Source Tracking](/surveys/link-surveys/source-tracking). - **URL**: The URL of the page where the user filled the survey. @@ -24,26 +24,15 @@ icon: "user" ## View Response Metadata -1. Go to the Responses tab of your survey. +Open the **Responses** tab of your survey and scroll the table to the right. Each metadata field is a column of its own, so a whole set of responses can be read at once. -2. Hover over the profile icon of the user on the response card & you should see a tooltip opening up with the metadata details. +![The metadata columns of the responses table](/images/surveys/general-features/metadata/response-metadata.webp) -![Metadata Card on Response Tab](/images/surveys/general-features/metadata/metadata-card.webp) +The gear icon above the table hides columns you do not want to see. ## Filter Responses by Metadata -1. Go to the Responses tab of your survey. - -2. Click on the Filter button. - -3. Scroll down & Select the metadata field you want to filter by. - -4. Select the condition & the value you want to filter by. - -![Apply Filters on Metadata](/images/surveys/general-features/metadata/filters.webp) - - -1. Now you should see the responses filtered based on the metadata you selected. If you want to see a walkthrough, view the video above to see how you can view & filter responses by metadata. +**Filter**, above the table, offers the same fields. Pick one, choose a condition and a value, and the table narrows to the responses that match — useful for questions like "what did the people who came from the newsletter say?". ## Export Metadata diff --git a/docs/surveys/general-features/overwrite-styling.mdx b/docs/surveys/general-features/overwrite-styling.mdx index c860acb0aeb1..70b90b8ceeec 100644 --- a/docs/surveys/general-features/overwrite-styling.mdx +++ b/docs/surveys/general-features/overwrite-styling.mdx @@ -12,19 +12,20 @@ Overwrite the global styling theme for individual surveys to create unique style ## Overwrite Styling Theme -1. In the **Survey Editor** of the survey you want to style, navigate to the **Styling** tab: +1. In the **Survey Editor** of the survey you want to style, open the **Styling** tab. The four sections below are greyed out. Until you override it, this survey follows the workspace theme. - ![Styling tab in survey editor](/images/surveys/general-features/overwrite-styling/step-nine.webp) + ![The Styling tab of the survey editor](/images/surveys/general-features/overwrite-styling/styling-tab.webp) -2. Activate the **Add Custom Styles** toggle to override the default Workspace styling: +2. Switch on **Add custom styles**. The sections become editable, and every change applies to this survey alone. - ![Add Custom Styles toggle](/images/surveys/general-features/overwrite-styling/step-ten.webp) + ![The Styling tab with custom styles switched on](/images/surveys/general-features/overwrite-styling/custom-styles.webp) -3. Customize your survey's style as needed: + - **Survey styling** — question text, descriptions and input fields. + - **Card styling** — the card the survey sits on. + - **Background styling** — a colour, an image or an animation behind it. + - **Logo settings** — whether the workspace logo shows on this survey. - ![Custom styling options](/images/surveys/general-features/overwrite-styling/step-eleven.webp) - -Just hit the **Save** button to apply your changes. Your survey is now ready to impress with its unique look! +3. Hit **Save**. To change the theme for *every* survey instead, use [Styling Theme](/platform/features/styling-theme). ## Overwrite CSS Styles for App & Website Surveys diff --git a/docs/surveys/general-features/quota-management.mdx b/docs/surveys/general-features/quota-management.mdx index 8d1790f68799..0cd7bcfb5880 100644 --- a/docs/surveys/general-features/quota-management.mdx +++ b/docs/surveys/general-features/quota-management.mdx @@ -12,6 +12,10 @@ Quota Management allows you to set limits on the number of responses collected f Quota Management is part of the [Enterprise Edition](/self-hosting/advanced/license). +Quotas live in the survey editor, on the **Settings** tab: + +![The Quotas section of the survey editor settings](/images/surveys/general-features/quota-management/quotas.webp) + ### Key benefits - **Balanced Data Collection**: Ensure your survey responses are evenly distributed across different segments diff --git a/docs/surveys/general-features/recall.mdx b/docs/surveys/general-features/recall.mdx index f4b6796e5f17..622773483f8d 100644 --- a/docs/surveys/general-features/recall.mdx +++ b/docs/surveys/general-features/recall.mdx @@ -17,27 +17,21 @@ You can recall data from the following sources: ## Recalling from a previous question - The recall functionality is disabled on the first question of the survey since - there’s no preceding question to recall data from. + On the first question the menu lists only hidden fields and variables — there is no earlier answer + to recall yet. -### **Pre-requisite** +### Step 1: Insert the recall -Ensure the answer you wish to recall precedes the question in which it will be recalled. Here’s an example of setting up the first question: +Type **`@`** in the question or description where the value should appear. A menu opens listing everything available at that point in the survey — the questions before this one, the hidden fields, and the variables. -![Survey setup example with link survey template](/images/surveys/general-features/recall/step-three.webp) +![The recall menu, listing earlier questions, hidden fields and variables](/images/surveys/general-features/recall/recall-menu.webp) -### **Step 1: Recall Data** +### Step 2: Set a fallback -Type **`@`** in the question or description field where you want to insert a recall. This triggers a dropdown menu listing all preceding questions. Select the question you want to recall data from. +Pick a source and the editor asks for a fallback: the text to show when there is nothing to recall. An optional question that was skipped, or a hidden field the URL never filled, would otherwise leave a gap in the sentence. -![Dropdown menu for recalling data in survey](/images/surveys/general-features/recall/step-two.webp) - -### **Step 2: Set a Fallback** - -To ensure the survey remains coherent when a response is missing (or the question is optional), you should set a fallback option. - -![Setting fallback option in survey question](/images/surveys/general-features/recall/step-one.webp) +![The fallback prompt that opens after picking a recall source](/images/surveys/general-features/recall/fallback.webp) ## Recalling from the URL @@ -56,7 +50,7 @@ To ensure the survey remains coherent when a response is missing (or the questio 2. Use the `@` symbol in a question or description to recall the value of the variable -3. Set a fallback in case the variable is not being filled by a URL parameter +3. Set a fallback in case the variable has not been calculated yet when the question is shown ## Live Demo diff --git a/docs/surveys/general-features/spam-protection.mdx b/docs/surveys/general-features/spam-protection.mdx index 036349e18fa1..c6cb1b542378 100644 --- a/docs/surveys/general-features/spam-protection.mdx +++ b/docs/surveys/general-features/spam-protection.mdx @@ -79,7 +79,7 @@ You can enable Google reCAPTCHA v3 spam protection for your survey directly from Adjust the **response threshold**. This is the score threshold for accepting or rejecting responses. A lower threshold (e.g., 0.1) is lenient, while a higher threshold (e.g., 0.9) is strict. -![Set reCAPTCHA Threshold](/images/surveys/general-features/spam-protection/spam-protection.webp) +![Set reCAPTCHA Threshold](/images/surveys/general-features/spam-protection/response-option.webp) diff --git a/docs/surveys/general-features/survey-scheduling.mdx b/docs/surveys/general-features/survey-scheduling.mdx new file mode 100644 index 000000000000..01385950489d --- /dev/null +++ b/docs/surveys/general-features/survey-scheduling.mdx @@ -0,0 +1,45 @@ +--- +title: "Schedule a Survey" +description: "Publish a survey on a future date and close it on another, so a survey runs for a fixed window without anyone having to remember to switch it on or off." +icon: "calendar-days" +--- + +A survey can open and close on dates you set in advance. Use it when the window matters — a quarterly pulse, a survey that should not start before a launch, or a panel you want to stop collecting after a deadline. + +Both settings are independent. You can schedule only the start, only the end, or both. + +## Setting the dates + +1. Open the survey in the editor and go to the **Settings** tab. +2. Open **Response Options**. +3. Turn on **Publish survey on date**, **Close survey on date**, or both, and pick a date for each. + +![The publish and close date settings in the survey editor](/images/surveys/general-features/survey-scheduling/publish-and-close.webp) + +## What the dates mean + +Both actions happen at a fixed time of day, in a fixed time zone, on the date you choose — not at the moment you set them. + +- Before its publish date, the survey shows as **Scheduled** in the survey list and the status dropdown. It is a paused survey with a future publish date rather than a status of its own, so pausing and scheduling are the same underlying state. +- A survey that reaches its close date stops accepting responses, exactly as if you had closed it by hand. Existing responses are untouched. +- Removing a date with the **×** next to it cancels that half of the schedule. + + + The time of day and the time zone are properties of the Formbricks instance, not of the survey. The editor tells you which apply — for example "Survey will be published at 00:00 in the Europe/Berlin timezone on the selected date". + + +## Self-hosting + +Self-hosted instances can change when scheduled surveys open and close: + +| Variable | What it does | Default | +| --- | --- | --- | +| `SURVEY_SCHEDULING_TIME_ZONE` | The IANA time zone the scheduled time is interpreted in | `Europe/Berlin` | +| `SURVEY_SCHEDULING_LOCAL_HOUR` | Hour of day, 0–23 | `0` | +| `SURVEY_SCHEDULING_LOCAL_MINUTE` | Minute of the hour, 0–59 | `0` | + +Set them to match the time zone your respondents live in, so "closes on the 30th" means what your team means by it. The editor's summary text updates to show the values in force. + + + Scheduling depends on the job runner. See [Job Runner](/self-hosting/configuration/job-runner) if scheduled surveys are not opening or closing on time. + diff --git a/docs/surveys/general-features/tags.mdx b/docs/surveys/general-features/tags.mdx index 15554ccb5632..5c6c2d0b3cb0 100644 --- a/docs/surveys/general-features/tags.mdx +++ b/docs/surveys/general-features/tags.mdx @@ -13,7 +13,7 @@ Tags are labels that you can apply to individual survey responses. They allow yo - Track and organize feedback across multiple surveys - Simplify analysis and reporting workflows -Tags are environment-specific, meaning each environment maintains its own set of tags. +Tags are workspace-specific, meaning each workspace maintains its own set of tags. ## Add tags to responses @@ -49,18 +49,20 @@ The tag will be removed from the response immediately. ## Manage tags -Access the tag management page to view and organize all tags in your environment. +Access the tag management page to view and organize all tags in your workspace. - - Click on **Workspace Configuration** > **Tags**. + + Go to **Settings → Workspace → Tags**. - You'll see a list of all tags in your environment with their usage count showing how many responses have each tag applied. + You'll see a list of all tags in your workspace with their usage count showing how many responses have each tag applied. +![The tag manager, listing each tag with its usage count](/images/surveys/general-features/tags/manager.webp) + ### Edit tag names 1. In the tag management page, click on the tag name field @@ -68,7 +70,7 @@ Access the tag management page to view and organize all tags in your environment 3. Click outside the field or press Enter to save -Tag names must be unique within an environment. If you try to use an existing tag name, you'll receive an error. +Tag names must be unique within a workspace. If you try to use an existing tag name, you'll receive an error. ### Merge tags diff --git a/docs/surveys/general-features/validation-rules.mdx b/docs/surveys/general-features/validation-rules.mdx index eb38ec581c69..74c8e3fc70a2 100644 --- a/docs/surveys/general-features/validation-rules.mdx +++ b/docs/surveys/general-features/validation-rules.mdx @@ -6,7 +6,7 @@ icon: "check-double" By adding validation rules to your questions, you can improve data quality, reduce errors, and create a better survey experience. -![Validation Rules Editor](/images/xm-and-surveys/core-features/validation-rules/editor.webp) +![The Validation rules section of a free text question](/images/surveys/general-features/validation-rules/editor.webp) ## How Validation Rules Work @@ -143,11 +143,11 @@ Each contact field can have specific validation rules: Click on the question you want to validate to open its settings panel. - - Scroll down to find the "Validation Rules" section and click to expand it. + + Find the **Validation rules** switch in the question's settings and turn it on. The first rule appears with it. - - Click the "Add rule" button to add a new validation rule. + + The `+` at the end of a rule row adds one below it; the bin removes one. Select the rule type from the dropdown and enter the required value (if applicable). diff --git a/docs/surveys/general-features/variables.mdx b/docs/surveys/general-features/variables.mdx index c70f0b9415bc..23b1b0947764 100644 --- a/docs/surveys/general-features/variables.mdx +++ b/docs/surveys/general-features/variables.mdx @@ -14,17 +14,12 @@ There are two types of variables you can add to your survey: ## How to Add Variables -1. Edit the survey you want to add variables to & switch to the Questions tab and scroll down to the bottom of the page. You will see a section called **Variables**. +1. Open the survey in the editor, stay on the **Questions** tab and scroll to the bottom. You will find a **Variables** section. +2. Give the variable a name, choose its type — text or number — and set a starting value. Add as many as you need. -![Variables card](/images/surveys/general-features/variables/variables-card.webp) +Each variable also gets an ID, listed under **Variable IDs**, which is what you use to recall it. - -1. Now click on it to add a new variable ID. You can add as many variables as you want. You can also choose the type of variable you want to add along with the default value. - -![add variables](/images/surveys/general-features/variables/input-variables.webp) - - -![created variables](/images/surveys/general-features/variables/created-variables.webp) +![Variables in the survey editor](/images/surveys/general-features/variables/editor.webp) ## Use cases @@ -36,7 +31,7 @@ There are two types of variables you can add to your survey: Variables are different from hidden fields in the following ways: -1. **Setting**: Hidden fields can be set through query parameters or `formbricks.init`, but the variables can only be set either during creation or dynamically by using logic actions. +1. **Setting**: Hidden fields can be set through query parameters or `formbricks.setup`, but variables can only be set either during creation or dynamically by using logic actions. 2. **Updating**: Hidden fields cannot be set again, but the value of variables can be updated while the user fills the survey. diff --git a/docs/surveys/link-surveys/data-prefilling.mdx b/docs/surveys/link-surveys/data-prefilling.mdx index 3aa7544cc7b8..cccb3476dea2 100644 --- a/docs/surveys/link-surveys/data-prefilling.mdx +++ b/docs/surveys/link-surveys/data-prefilling.mdx @@ -43,7 +43,7 @@ Formbricks lets you prefill as many values as you want. Combine multiple values ### Where do I find my question Id? -You can find the `questionId` in the **Advanced Settings** at the bottom of each question card in the Survey Editor. You can update the `questionId` to any string you like **before you publish a survey.** After you published a survey, you cannot change the id anymore. +Open a question in the Survey Editor and click **Show Question settings** at the foot of its card. The `questionId` is the first field there. You can change it to any string you like **before you publish a survey**; after publishing it is fixed, because the responses already collected refer to it. ![The question Id is located at the bottom of each question card in the survey editor.](/images/surveys/link-surveys/data-prefilling/question-id.webp) diff --git a/docs/surveys/link-surveys/personal-links.mdx b/docs/surveys/link-surveys/personal-links.mdx index fdbc056a6110..58430aab4546 100644 --- a/docs/surveys/link-surveys/personal-links.mdx +++ b/docs/surveys/link-surveys/personal-links.mdx @@ -35,6 +35,8 @@ When you generate personal links: In the Share Modal, click on the **Personal Links** tab. + + ![The Personal links tab of the Share modal](/images/surveys/link-surveys/personal-links/generate.webp) diff --git a/docs/surveys/link-surveys/pin-protected-surveys.mdx b/docs/surveys/link-surveys/pin-protected-surveys.mdx index 0b6981477fb2..e53170d72a9c 100644 --- a/docs/surveys/link-surveys/pin-protected-surveys.mdx +++ b/docs/surveys/link-surveys/pin-protected-surveys.mdx @@ -5,64 +5,34 @@ description: icon: "lock" --- -## **Enabling PIN Protection** +A PIN keeps a link survey from being answered by anyone who happens to have the URL. The link still works; without the four digits it goes no further than a prompt. -PIN protection can be applied to your surveys easily through the survey editor. This setup allows you to control access effectively, ensuring that only authorized users can participate. +## Enabling PIN protection -### **Steps to Set Up PIN Protection** +Open the survey editor, go to the **Settings** tab, and expand **Response Options**. Switch on **Protect survey with a PIN** and type the four digits respondents will need. -1. **Open Settings in Survey Editor**: Navigate to your survey in the survey editor where you wish to enable PIN protection & click on Settings Tab. +![The PIN row of Response Options](/images/surveys/link-surveys/pin-protected-surveys/response-option.webp) -2. **Select Response Options**: Find and select **`Response Options`** to access settings related to survey responses. +The PIN can be changed at any time from the same place. Changing it locks out anyone still holding the old one. - ![Select Response Options](/images/surveys/link-surveys/pin-protected-surveys/step-one.webp) +## What the respondent sees -1. **Enable PIN Protection**: Find the option for "Protect Survey with a PIN" and - activate it. You will be prompted to enter a PIN that respondents must use to access - the survey. +Opening the link shows the prompt instead of the first question. -### **Setting the PIN** +![The PIN prompt a respondent sees](/images/surveys/link-surveys/pin-protected-surveys/pin-prompt.webp) -![Choose a link survey template](/images/surveys/link-surveys/pin-protected-surveys/step-two.webp) +A wrong PIN is rejected and can be retried; the right one opens the survey as usual. -Enter the PIN you wish to use for your survey. Once set, this PIN will need to be -entered by participants to access the survey. Note that this can be changed anytime -from here in the future. +## When to use it -### **User Experience Upon Accessing the Survey** +- Confidential surveys — internal company feedback, or research where the questions themselves are sensitive. -When a respondent attempts to access the survey, they are prompted to enter the PIN: +- Surveys for one event or one group, where you can hand the PIN out in the room. -- **PIN Entry Prompt**: A screen will appear asking the respondent to enter the PIN to proceed. This acts as the first gatekeeping step before survey access is granted. +- Surveys where the link is likely to be forwarded and you would rather it went no further. - ![Choose a link survey template](/images/surveys/link-surveys/pin-protected-surveys/step-three.webp) - -- **Incorrect PIN Handling**: If an incorrect PIN is entered, the respondent will be informed and asked to try again, ensuring secure access to the survey. - - ![Choose a link survey template](/images/surveys/link-surveys/pin-protected-surveys/step-four.webp) - -- **Correct PIN**: On entering the correct PIN, the user access the survey & can fill it accordingly. - - ![Choose a link survey template](/images/surveys/link-surveys/pin-protected-surveys/step-five.webp) - -### **Benefits of PIN Protection** - -- **Enhanced Security**: Protects the survey from unauthorized access, ensuring that only participants with the PIN can enter. - -- **Controlled Participation**: Enables you to restrict survey participation to a specific group, such as during a closed testing phase or confidential feedback gathering. - -- **Prevents Unwanted Access**: Deters casual browsing and unauthorized attempts to view or complete the survey. - -## **Use Cases** - -PIN protection is particularly useful in situations where: - -- Confidential surveys are being conducted, such as internal company feedback or sensitive research studies. - -- Surveys are designed for a specific event or group, and access needs to be controlled. - -- You want to limit survey responses to participants who have been explicitly invited or have registered in advance. - -## **Conclusion** - -Setting up PIN protection for your Formbricks surveys is a straightforward and effective way to ensure that only authorized respondents can access and complete your surveys. This feature adds an additional layer of security and control, making it ideal for managing access to sensitive or exclusive surveys. + + A PIN gates access, it does not identify anyone. Everyone who has it shares it. If you need to know + *which* person answered, use [personal links](/surveys/link-surveys/personal-links) or + [single-use links](/surveys/link-surveys/single-use-links) instead. + diff --git a/docs/surveys/link-surveys/pretty-url.mdx b/docs/surveys/link-surveys/pretty-url.mdx index ce5392fab0d8..6c6708501089 100644 --- a/docs/surveys/link-surveys/pretty-url.mdx +++ b/docs/surveys/link-surveys/pretty-url.mdx @@ -27,6 +27,8 @@ When someone visits the pretty URL, they are automatically redirected to the act In the Share Modal, select the **Pretty URL** tab. + + ![The Pretty URL tab of the Share modal](/images/surveys/link-surveys/pretty-url/custom-slug.webp) @@ -54,7 +56,7 @@ All surveys that have a pretty URL assigned are listed in one place: 1. Go to **Settings → Organization → Domain**. 2. Open the **Pretty URLs** section. -The table shows each survey's name, workspace, slug, and environment type (production / development). +The table has three columns — **Survey Name**, **Workspace**, and **Pretty URL** — with one row per survey that has a slug. ## Slug Rules diff --git a/docs/surveys/link-surveys/source-tracking.mdx b/docs/surveys/link-surveys/source-tracking.mdx index df059d7d37ed..47f4a735fbfe 100644 --- a/docs/surveys/link-surveys/source-tracking.mdx +++ b/docs/surveys/link-surveys/source-tracking.mdx @@ -31,9 +31,9 @@ https://formbricks.com/s/clin3dxja02k8l80hpwmx4bjy?source=Google 2. **Collect Data**: When users access the survey through these links, the URL parameters will capture the source information from which they were shared. -3. **View Responses**: Use the collected source data to analyze where your survey respondents are coming from. You can hover over the user icon in the responses tab to see the source of the user. +3. **View Responses**: The value lands in the response's metadata, alongside the URL, browser, OS, device and country Formbricks records anyway. Open the **Responses** tab and scroll the table right to reach those columns. - ![View Source in Response](/images/surveys/link-surveys/source-tracking/view-response.webp) + ![The metadata columns of the responses table](/images/surveys/link-surveys/source-tracking/responses-table.webp) 4. **Analyse Data**: Download all the responses as a CSV/Excel and get access to the source information. This can provide valuable insights into your audience. diff --git a/docs/surveys/link-surveys/verify-email-before-survey.mdx b/docs/surveys/link-surveys/verify-email-before-survey.mdx index 3dc9e762706b..4b9a26438362 100644 --- a/docs/surveys/link-surveys/verify-email-before-survey.mdx +++ b/docs/surveys/link-surveys/verify-email-before-survey.mdx @@ -5,64 +5,45 @@ description: icon: "envelope" --- -To ensure the credibility of your survey participants and maintain high-quality data, you can require respondents to verify their email before they can view and respond to your Formbricks link surveys. This verification process helps confirm that only participants with valid email addresses can access the survey, enhancing data integrity. +Requiring a verified email address before someone can answer records, with every response, an address the respondent could read mail at. -## **Enabling Email Verification** + + Verification proves access to that mailbox and nothing more. It does not establish who the person is, + and a disposable inbox passes it as readily as a corporate one. Treat it as a way to attribute a + response to an address, not as proof of identity. + -This feature, designed for link surveys, can be enabled or disabled directly from the survey editor. Here’s how to activate it: +## Enabling email verification -### **Steps to Enable Email Verification** +Open the survey editor, go to the **Settings** tab, and expand **Response Options**. Switch on **Verify email before submission**. -1. **Open Survey Editor**: Navigate to your link survey in the survey editor where you wish to enable email verification. -2. **Access Settings**: Click on the **`Settings`** tab next to the Questions & Styling tab. -3. **Select Response Options**: Find and select **`Response Options`** to access settings related to survey responses. +![The email verification row of Response Options](/images/surveys/link-surveys/verify-email-before-survey/response-option.webp) - ![Choose a link survey template](/images/surveys/link-surveys/verify-email-before-survey/step-one.webp) + + This is a link survey feature. App surveys already know who the respondent is. + -4. **Activate Email Verification**: Find the "Verify Email Before Accessing Survey" option and use the toggle to activate email verification. Specify what details should be visible to the public when they access the survey. +## What the respondent sees - ![Choose a link survey template](/images/surveys/link-surveys/verify-email-before-survey/step-two.webp) +Opening the link asks for an address first. Formbricks sends a link to that address, and the survey opens once it is followed. -### **User Experience Upon Accessing the Survey** +![The email gate a respondent sees](/images/surveys/link-surveys/verify-email-before-survey/email-gate.webp) -When email verification is enabled, the following process unfolds for the user: +**Preview survey questions** lets someone read the survey without verifying anything. They cannot answer — it is there so a cautious respondent can see what they are being asked before handing over an address. -1. **Email Entry Prompt**: Upon accessing the survey link, the user is prompted to verify their email before they can proceed. +## Where the verified address shows up - ![Choose a link survey template](/images/surveys/link-surveys/verify-email-before-survey/step-three.webp) +Each response carries the address it was verified against, visible on the response in the Responses tab and included in exports. -2. **Preview Option**: A "Preview survey questions" option is available for those who are just browsing or curious about the survey content without completing it. This allows a non-interactive view of the survey. +## When to use it - ![Choose a link survey template](/images/surveys/link-surveys/verify-email-before-survey/step-four.webp) +- Market research that has to come from a known population. -3. **Verification Process**: After entering their email, respondents receive an email containing a survey link, which they can use to access the survey. +- Surveys behind gated content, where the audience is a list you already hold. - ![Choose a link survey template](/images/surveys/link-surveys/verify-email-before-survey/step-five.webp) +- Academic or professional studies where a response has to be attributable. -4. **Survey Access**: After verifying their email, respondents can access and respond to the survey. - -![Choose a link survey template](/images/surveys/link-surveys/verify-email-before-survey/step-six.webp) - -### **Benefits of Email Verification** - -- **Authenticity of Respondents**: Ensures that each respondent is verified, adding an extra layer of authenticity to your data collection. -- **Reduction in Spam and Fraudulent Responses**: Helps reduce unwanted or spam entries by confirming the legitimacy of each respondent’s email address. -- **Enhanced Data Quality**: Increases the reliability and quality of the data collected by confirming the identity of participants. - -### **Visibility of Verified Emails** - -In the Formbricks dashboard, the survey response card displays the verified email along with the responses attached to it, ensuring traceability and authenticity of the data collected. - -![Choose a link survey template](/images/surveys/link-surveys/verify-email-before-survey/step-seven.webp) - -## **Use Cases** - -Email verification is particularly beneficial in scenarios where: - -- Market research requires verified responses from specific demographics. -- Surveys are intended for a selected audience with gated content. -- Academic or professional studies necessitate authenticated data for accuracy. - -## **Conclusion** - -Implementing the Email Verification feature is an effective strategy to ensure the authenticity of your survey respondents. By requiring email verification before survey access, you safeguard the integrity of your survey data and ensure that only verified individuals can contribute responses. + + Verification needs a working SMTP configuration. On a self-hosted instance, see [SMTP + Configuration](/self-hosting/configuration/smtp). + diff --git a/docs/surveys/question-type/ces.mdx b/docs/surveys/question-type/ces.mdx new file mode 100644 index 000000000000..316edf815f5d --- /dev/null +++ b/docs/surveys/question-type/ces.mdx @@ -0,0 +1,49 @@ +--- +title: "Customer Effort Score (CES)" +description: "CES asks how much effort a task took, usually as agreement with a statement. Use it after something the customer had to get done." +icon: "gauge-simple" +--- + +CES measures how hard something was, not how much someone liked it. It is the right question after a task with a clear finish line — opening an account, changing a plan, getting a refund — because effort predicts whether people come back better than satisfaction does. + +Phrase it as a statement to agree or disagree with, not a question. "Opening my account was easy" reads more naturally on an agreement scale than "How easy was opening your account?". + +## Elements + +![The CES question type in the survey editor](/images/surveys/question-type/ces/editor.webp) + +### Title + +Write the statement the respondent is agreeing or disagreeing with, and keep it to one task. A statement covering two steps produces an answer you cannot act on. + +### Description + +Provide an optional description with further instructions. + +### Scale + +Choose how the points are displayed: + +| Scale | When to use | +| --- | --- | +| **Number** | The usual choice for CES, because agreement scales are conventionally numeric. | +| **Smiley** | Faster to answer, though it reads as satisfaction rather than effort. | +| **Star** | Rarely the right fit — stars imply quality, not ease. | + +### Range + +CES supports **5** or **7** points. Seven is the more common convention for agreement scales and gives respondents a finer choice; five is quicker to answer and easier to read on a phone. Pick one and keep it, since scores from different ranges are not comparable. + +### Labels + +Add labels for the lower and upper ends, for example "Strongly disagree" and "Strongly agree". + +### Add color coding + +Adds red, orange and green colour codes to the options. + +## How to choose between CES, CSAT and NPS + +- **CES** — how much *effort* did this take? Ask after a task the customer had to complete. +- **[CSAT](/surveys/question-type/csat)** — how did *this interaction* go? Ask straight after it. +- **[NPS](/surveys/question-type/net-promoter-score)** — how do you feel about *the company*? Ask periodically, not after a single event. diff --git a/docs/surveys/question-type/csat.mdx b/docs/surveys/question-type/csat.mdx new file mode 100644 index 000000000000..ea1eeb63d394 --- /dev/null +++ b/docs/surveys/question-type/csat.mdx @@ -0,0 +1,54 @@ +--- +title: "Customer Satisfaction (CSAT)" +description: "CSAT questions ask how satisfied someone was with a specific interaction, on a fixed five-point scale. Use it right after the thing you are asking about." +icon: "face-smile" +--- + +CSAT measures satisfaction with one specific experience — an order, a support conversation, an onboarding step — rather than a feeling about your company as a whole. Ask it while the interaction is still fresh. + + + CSAT always uses a **five-point** scale. If you need a different range, use a [Rating](/surveys/question-type/rating) question instead. + + +## Elements + +![The CSAT question type in the survey editor](/images/surveys/question-type/csat/editor.webp) + +### Title + +Name the interaction you are asking about, not the company. "How satisfied were you with this support conversation?" gets a more useful answer than "How satisfied are you with us?". + +### Description + +Provide an optional description with further instructions. + +### Scale + +Choose how the five points are displayed: + +| Scale | When to use | +| --- | --- | +| **Smiley** | Fastest to answer, and reads the same in every language. The default choice for consumer-facing surveys. | +| **Number** | When you want respondents to think of the answer as a score, or when smileys clash with a formal tone. | +| **Star** | Familiar from review sites, so it reads as a rating of quality. | + +### Labels + +Add labels for the lower and upper ends of the scale, for example "Very unsatisfied" and "Very satisfied". Both are optional, but a scale without them leaves the respondent guessing which end is good. + +### Add color coding + +Adds red, orange and green colour codes to the options, so the scale reads at a glance. + +## CSAT and the rating question + +A CSAT question and a five-point [Rating](/surveys/question-type/rating) question look similar in a survey. The difference is what they are for: + +- **CSAT** is a standard metric with a fixed scale, so scores are comparable across surveys and over time. +- **Rating** is flexible — you choose the range — which makes it better for one-off questions and worse for tracking a trend. + +## How to choose between CSAT, NPS and CES + +- **CSAT** — how did *this interaction* go? Ask straight after it. +- **[NPS](/surveys/question-type/net-promoter-score)** — how do you feel about *the company*? Ask periodically, not after a single event. +- **[CES](/surveys/question-type/ces)** — how much *effort* did this take? Ask after a task the customer had to complete. diff --git a/docs/surveys/website-app-surveys/actions.mdx b/docs/surveys/website-app-surveys/actions.mdx index 9e90694d7f12..558d970d5f70 100644 --- a/docs/surveys/website-app-surveys/actions.mdx +++ b/docs/surveys/website-app-surveys/actions.mdx @@ -57,54 +57,48 @@ Formbricks offers an intuitive No-Code interface that allows you to configure ac - - ![Action overview on Formbricks Open Source Survey Solution](/images/surveys/website-app-surveys/actions/actions-view.webp "Action overview on Formbricks Open Source Survey Solution") - + + Every action in the workspace is listed here, no-code and code alike. - - ![Add action to open source in app survey](/images/surveys/website-app-surveys/actions/i2.webp "Add action to open source in app survey") + ![The User Actions list](/images/surveys/website-app-surveys/actions/user-actions.webp "The User Actions list") - - -There are four types of No-Code actions: - -### 1. Click action - -![Add click action to open source in app survey](/images/surveys/website-app-surveys/actions/click-action.webp "Add click action to open source in app survey") -A Click Action is triggered when a user clicks on a specific element within your application. You can define the element's inner text, CSS selector or both to trigger the survey. + + Name the action, say what the user is doing, and — optionally — limit it to certain pages. -- **Inner Text**: Checks if the innerText of a clicked HTML element, like a button label, matches a specific text. This action allows you to display a survey based on text interactions within your application. + ![The Track New User Action dialog](/images/surveys/website-app-surveys/actions/add-action.webp "The Track New User Action dialog") + + -- **CSS Selector**: Verifies if a clicked HTML element matches a provided CSS selector, such as a class, ID, or any other CSS selector used in your website. It enables survey triggers based on element interactions. +There are five types of no-code action. -- **Both**: Only if both is true, the action is triggered +### 1. Click -### 2. Page view action +Fires when a user clicks a specific element. Define the element by its **CSS Selector**, its **Inner Text**, or both — with both switched on, the click has to match both to count. -![Add page view action to open source in app survey](/images/surveys/website-app-surveys/actions/page-view.webp "Add page view action to open source in app survey") +- **Inner Text**: matches the text of the clicked element, such as a button label. -This action is triggered when a user visits a page within your application. +- **CSS Selector**: matches a class, an id, or any other CSS selector. -### 3. Exit intent action +### 2. Page View -![Add exit intent action to open source in app survey](/images/surveys/website-app-surveys/actions/exit-intent.webp "Add exit intent action to open source in app survey") +Fires when a user opens a page. On its own it fires on every page; combine it with a page filter to narrow it down. -This action is triggered when a user is about to leave your application. It helps capture user feedback before they exit, providing valuable insights into user experiences and potential improvements. +### 3. Exit Intent -### 4. 50% scroll action +Fires when a user looks about to leave — the pointer leaves the viewport. It is how you catch someone before they abandon a form. -![Add 50% scroll action to open source in app survey](/images/surveys/website-app-surveys/actions/scroll.webp "Add 50% scroll action to open source in app survey") +### 4. 50% Scroll -This action is triggered when a user scrolls through 50% of a page within your application. It helps capture user feedback at a specific point in their journey, enabling you to gather insights based on user interactions. +Fires once a user has scrolled halfway down a page. A rough proxy for "read it". -This action is triggered when a user visits a specific page within your application. You can define the URL match conditions as follows: +### 5. Time on Page -You can combine the url filters with any of the no-code actions to trigger the survey based on the URL match conditions. +Fires after a user has stayed on a page for a number of seconds that you set. ### Page filter -You can limit action tracking to specific subpages of your website or web app by using the Page Filter. Here you can use a variety of URL filter settings: +Every no-code action can be limited to certain pages. Leave it on **On all pages**, or switch to **Limit to specific pages** and add one or more URL rules: - **exactMatch**: Triggers the action when the URL exactly matches the specified string. @@ -120,15 +114,21 @@ You can limit action tracking to specific subpages of your website or web app by - **matchesRegex**: Activates when the URL matches the pattern from the specified string. +## Triggering a survey with an Action + +An action does nothing until a survey listens for it. In the survey editor, open **Settings → Survey Trigger** and add one. A survey can listen for several actions; any one of them is enough to show it. + +![The Survey Trigger section with one action attached](/images/surveys/website-app-surveys/actions/survey-trigger.webp "The Survey Trigger section with one action attached") + ## Setting up code Actions For more granular control, you can implement actions directly in your code: - First, add the action via the Formbricks web interface to make it available for survey configuration: + First, add the action via the Formbricks web interface to make it available for survey configuration. Switch the dialog to **Code** and give the action a key — that key is the string your code will send. -![Add a code action to open source in app survey](/images/surveys/website-app-surveys/actions/code-action.webp "Add a code action to open source in app survey") +![The Track New User Action dialog on its Code tab](/images/surveys/website-app-surveys/actions/code-action.webp "The Track New User Action dialog on its Code tab") diff --git a/docs/surveys/website-app-surveys/attribute-based-targeting.mdx b/docs/surveys/website-app-surveys/attribute-based-targeting.mdx index 457942ba3dc8..4732dab5a14c 100644 --- a/docs/surveys/website-app-surveys/attribute-based-targeting.mdx +++ b/docs/surveys/website-app-surveys/attribute-based-targeting.mdx @@ -21,26 +21,28 @@ Attribute-based Targeting helps you achieve a number of goals: ## How does Attribute-based Targeting work? - - To get started, go to the Contacts tab and create a new Segment: - - ![Create a new segment](/images/surveys/website-app-surveys/targeting/contacts.webp "Create a new segment") + + Go to **Contacts → Segments** and click `Create segment`. + ![The Segments list](/images/surveys/website-app-surveys/targeting/segments-list.webp "The Segments list") - - In the Segment editor, you can configure your Segment with a combination of Attributes, Segments, Devices, and Survey Interactions. If a user matches either or all of the criteria, they become part of the Segment. See [Segment Configuration](/surveys/website-app-surveys/attribute-based-targeting#segment-configuration) below. + + Open the segment and switch to its **Settings** tab. Everything under **Targeting** decides who is in it. + + ![A segment's Settings tab](/images/surveys/website-app-surveys/targeting/segment-editor.webp "A segment's Settings tab") - - Create a new survey and go to Settings to change it to Website & App survey: + + A segment can only target a survey that runs inside your product. In the survey editor, open **Settings → Survey Type**. - ![Create a new segment of type in-app](/images/surveys/website-app-surveys/targeting/survey-type.webp "Create a new segment") + ![The Survey Type section](/images/surveys/website-app-surveys/targeting/survey-type.webp "The Survey Type section") - - ![Choose Segment in Targeting options](/images/surveys/website-app-surveys/targeting/target-audience.webp "Choose Segment in Targeting options") + + Still in **Settings**, open **Target Audience** and pick the segment. + ![The Target Audience section](/images/surveys/website-app-surveys/targeting/target-audience.webp "The Target Audience section") @@ -48,22 +50,18 @@ Attribute-based Targeting helps you achieve a number of goals: ### Segment Configuration -There are four means to move Contacts in or out of Segments: **Attributes**, other **Segments**, **Devices**, and [**Survey Interactions**](/surveys/website-app-surveys/survey-display-logic#level-4-interaction-based-segments): - -1. **Attributes**: If the value of a specific attribute matches, the user becomes part of the Segment. +`Add filter` opens one list holding every kind of filter a segment can use. - ![Attribute filter](/images/surveys/website-app-surveys/targeting/attribute-filter.webp "Attribute filter") +![The Add filter dialog](/images/surveys/website-app-surveys/targeting/add-filter.webp "The Add filter dialog") +There are four means to move Contacts in or out of Segments: **Attributes**, other **Segments**, **Devices**, and [**Survey Interactions**](/surveys/website-app-surveys/survey-display-logic#level-4-interaction-based-segments): -2. **Segments**: You can nest Segments meaning that if a user is or is not part of another Segment, they can be included or excluded - - ![Segments filter](/images/surveys/website-app-surveys/targeting/segments-filter.webp "Segments filter") - +1. **Attributes**: If the value of a specific attribute matches, the user becomes part of the Segment. -3. **Devices**: If a user uses a Phone or Desktop, you can include or exclude them +2. **Segments**: You can nest Segments meaning that if a user is or is not part of another Segment, they can be included or excluded. - ![Devices filter](/images/surveys/website-app-surveys/targeting/device-filter.webp "Devices filter") +3. **Devices**: If a user uses a Phone or Desktop, you can include or exclude them. 4. **Survey Interactions**: Include or exclude Contacts based on whether they have seen, started, or completed a survey within a recent time window. See [Survey Display Logic](/surveys/website-app-surveys/survey-display-logic#level-4-interaction-based-segments) for details and use cases like per-survey cooldowns and follow-up funnels. -5. **Filter Groups:** You can group any of the above conditions in group and connect them logically with `AND` or `OR`. This allows for maximum granularity. \ No newline at end of file +Any of those four can be combined. Conditions are joined with `AND` or `OR`, and a **filter group** wraps several of them so the two can be mixed without ambiguity — the same reason brackets exist in arithmetic. \ No newline at end of file diff --git a/docs/surveys/website-app-surveys/cooldown-period.mdx b/docs/surveys/website-app-surveys/cooldown-period.mdx index ba0f51bf0a38..fdfca4915958 100644 --- a/docs/surveys/website-app-surveys/cooldown-period.mdx +++ b/docs/surveys/website-app-surveys/cooldown-period.mdx @@ -16,6 +16,8 @@ To adjust the workspace-wide Cooldown Period: 2. Find the **Cooldown Period (across surveys)** section. 3. Set the interval, in days. +![The workspace-wide Cooldown Period setting](/images/surveys/website-app-surveys/cooldown-period/workspace-setting.webp) + After a user sees *any* survey, no survey will be shown to them again until the Cooldown Period has elapsed. ## Overriding the Cooldown Period for a survey diff --git a/docs/surveys/website-app-surveys/recontact.mdx b/docs/surveys/website-app-surveys/recontact.mdx index 0399ed6c0466..6326592c5ed8 100644 --- a/docs/surveys/website-app-surveys/recontact.mdx +++ b/docs/surveys/website-app-surveys/recontact.mdx @@ -14,6 +14,8 @@ Recontact Options are the **per-survey** control for how often *this* survey may 2. Ensure the survey type is set to **App Survey**. 3. Open the **Visibility & Recontact** section and choose an option under **Recontact options**. +![The Visibility & Recontact section](/images/surveys/website-app-surveys/recontact/visibility-and-recontact.webp) + Available options: - **Show only once** *(default)* — show a single time, even if the user does not respond. @@ -27,6 +29,8 @@ Each option counts *this* survey's own displays and responses only. Recontact Options decide how often a survey may repeat; the [Cooldown Period](/surveys/website-app-surveys/cooldown-period) is a separate, workspace-wide gate that limits how often a user sees *any* survey. Both must pass before a survey is shown — Recontact Options are evaluated first, then the Cooldown Period. +The same **Visibility & Recontact** section is where a survey says how it treats that workspace-wide gate: **Use Cooldown Period** (the default), **Ignore Cooldown Period** for a survey that may show even if another survey was shown recently, or **Set custom Cooldown Period** to override the workspace value for this survey only. + --- Still struggling or is something not working as expected? [Join us in GitHub Discussions](https://github.com/formbricks/formbricks/discussions) and we'd be glad to assist you! diff --git a/docs/surveys/website-app-surveys/show-survey-to-percent-of-users.mdx b/docs/surveys/website-app-surveys/show-survey-to-percent-of-users.mdx index a56876932a7a..8684c82c2d28 100644 --- a/docs/surveys/website-app-surveys/show-survey-to-percent-of-users.mdx +++ b/docs/surveys/website-app-surveys/show-survey-to-percent-of-users.mdx @@ -24,7 +24,7 @@ To target specific segments of your audience or manage survey exposure, Formbric Enter the desired percentage (from 0.01% to 100%) of users to whom the survey will be shown - ![Set percentage](/images/surveys/website-app-surveys/targeting/percentage.webp "Set percentage") + ![Set percentage](/images/surveys/website-app-surveys/show-survey-to-percent-of-users/display-settings.webp "Set percentage") diff --git a/docs/surveys/website-app-surveys/user-identification.mdx b/docs/surveys/website-app-surveys/user-identification.mdx index 854dc10d8501..ce271df3b3ce 100644 --- a/docs/surveys/website-app-surveys/user-identification.mdx +++ b/docs/surveys/website-app-surveys/user-identification.mdx @@ -62,7 +62,7 @@ formbricks.setAttributes({ **Note**: the number of different attribute classes (e.g., "Plan," "First Name," etc.) is currently limited - to 150 attributes per environment. + to 150 attributes per workspace. ### Setting User Language diff --git a/docs/unify-feedback/dashboards-charts.mdx b/docs/unify-feedback/dashboards-charts.mdx index 2da39ebc8987..c155771e1824 100644 --- a/docs/unify-feedback/dashboards-charts.mdx +++ b/docs/unify-feedback/dashboards-charts.mdx @@ -1,11 +1,19 @@ --- title: "Dashboards & Charts" +sidebarTitle: "Overview" description: "Visualize Feedback Records and group charts onto shareable dashboards." icon: "chart-line" --- Dashboards & Charts let you turn Feedback Records into visual analytics. A **Chart** is a single visualization scoped to one Feedback Dataset. A **Dashboard** is a grid of charts you can share with your team. +Charts read Feedback Records, not survey responses. A workspace therefore needs a **Feedback Dataset** and at least +one **Feedback Source** before any chart has data - see the guide below for that path end to end. + + + From the NPS template to a shareable dashboard, with three hands-on CX use cases. + + ## Charts Charts live under **Analyze → Analysis**. Each chart is a query plus a visualization config. @@ -51,5 +59,9 @@ From a dashboard you can: ## Requirements -- A Feedback Dataset with records. +- A Feedback Dataset linked to your workspace. +- At least one Feedback Source writing into it - a dataset with no source stays empty. - Workspace access to that dataset. + +If Analysis shows **"No feedback dataset linked"**, start with +[Creating your first charts](/unify-feedback/dashboards-charts/creating-your-first-charts). diff --git a/docs/unify-feedback/dashboards-charts/creating-your-first-charts.mdx b/docs/unify-feedback/dashboards-charts/creating-your-first-charts.mdx new file mode 100644 index 000000000000..76056d8b11ee --- /dev/null +++ b/docs/unify-feedback/dashboards-charts/creating-your-first-charts.mdx @@ -0,0 +1,291 @@ +--- +title: "Creating your first charts" +description: "Go from a survey template to a shareable NPS dashboard, and learn why Feedback Records - not survey responses - are what charts are built on." +icon: "rocket" +--- + +Charts in Formbricks are not built on survey responses. They are built on **Feedback Records**, which is why a +brand-new workspace shows an empty Analysis page even when your surveys already have hundreds of responses. + +This guide walks the whole path once: from the NPS template to a dashboard your team can read at a glance. + +## Responses and Feedback Records + +A **survey response** is one submission of one survey. It lives with that survey, keeps the exact shape of the +questions you asked, and is the right thing to read when you want to see what a single person said. + +A **Feedback Record** is one answer to one question, rewritten into a shape that is the same for every source. +Each record carries when it was collected, which source it came from, what kind of question it was +(`nps`, `csat`, `rating`, `text`, ...) and the answer itself. One response becomes several records - one per +answered question. + + + Feedback Records table showing NPS, Text and CSAT records from two different surveys side by side + + +That normalization is what makes records reportable: + +- **One shape across sources.** NPS from a survey, a CSV upload, or the API all become `nps` records, so a single + chart can cover all of them. +- **Measures that know what a score means.** Because the record is typed, Formbricks can offer `NPS: Score`, + `CSAT: Score` or `NPS: Detractors` as ready-made measures instead of asking you to build the math yourself. +- **Comparisons across surveys.** Two surveys pointed at the same dataset can sit on the same chart and the same + time axis. +- **Enrichment.** Open text is scored for sentiment and emotion as it arrives, which gives you dimensions you never + asked a question about. + +Creating records never changes or deletes your responses. The response stays exactly where it was; a record is an +additional, chart-friendly copy. + +## What you need before your first chart + +Three things have to exist, in this order. Skipping the first is the most common reason Analysis looks broken: + + + Analysis page showing the empty state: No feedback dataset linked + + + + + Datasets live at the **organization** level. Go to **Settings → Organization → Datasets** and click + **Create dataset**. Give it a name that describes the body of feedback rather than a single survey - most teams + start with one dataset such as "Voice of Customer". + + Under **Workspace access**, pick the workspaces that should be able to read it. A workspace can only be linked + to one dataset, so this choice decides where that workspace's charts get their data. + + + Create dataset dialog with a dataset name and the workspace access picker + + + The dataset then shows up in the list with the number of workspaces that can reach it. + + + Feedback Datasets settings page listing the Voice of Customer dataset as active with one workspace + + + Only Owners and Managers can create datasets. See [Feedback Datasets](/unify-feedback/feedback-datasets) for + archiving and access details. + + + + A source is what turns incoming data into records. Go to **Analyze → Feedback Data → Feedback Sources** and click + **Add feedback source**. + + + Add feedback source dialog offering Formbricks surveys, CSV import, API ingestion and MCP server + + + Pick **Formbricks surveys**, then **Select questions**, choose your survey and tick the questions you want to + report on. Formbricks detects each question's type for you, so an NPS question becomes an `nps` record without + any mapping work. + + Leave **Import historical responses** on to backfill the responses you already collected. With it off, only + responses submitted from now on become records. + + + + Charts live under **Analyze → Analysis**. Every chart is scoped to the dataset your workspace is linked to. + + + +Once a source is live, the Feedback Sources page tells you which dataset it writes into. Several sources can feed the +same dataset - that is the point. + + + Feedback Sources list with two survey connectors, both live-syncing into the Voice of Customer dataset + + +## Use case 1: An NPS program you can read at a glance + +The most common starting point. Create a survey from the **NPS Survey** template (**Surveys → New Survey**, search +for "NPS"), publish it, collect some responses, and connect it as a source using the steps above. + +Now open **Analyze → Analysis → Charts** and click **Create chart**. You can describe the chart in natural language +or build it by hand; the three charts below are built by hand so you can see exactly which controls do what. + + + Create chart dialog with the AI prompt box and the five chart types + + + + The AI builder needs **Smart functionality (AI)** enabled for your organization and a configured AI provider. See + [AI Features](/platform/features/ai-features). + + +### Chart 1: Overall NPS + +Your headline number. One value, no dimensions. + +- **Chart type:** Big Number +- **Measure:** `NPS: Score` + + + Big Number chart showing an overall NPS score of 25.83 + + +`NPS: Score` is the standard calculation - `((promoters − detractors) / NPS responses) × 100` - so the number is +comparable to whatever your team quotes today. + +### Chart 2: NPS over time + +A single score tells you where you are. The trend tells you whether what you did last quarter worked. + +- **Chart type:** Line Chart +- **Measure:** `NPS: Score` +- **Add time-based grouping:** on + - **Field:** Collected At + - **Granularity:** Month + - **Date Range:** This year + + + Line chart of NPS: Score by month, rising from below zero in January to the mid-fifties in August + + + + The default date range is **Last 30 days**, which is why a brand-new trend chart often shows a single point. Widen + it before you conclude the chart is broken. + + +### Chart 3: Promoters, passives and detractors + +The score hides the shape of your base. A ten-point NPS made of loud promoters and loud detractors is a different +business problem from one made of shrugs. + +- **Chart type:** Bar Chart +- **Measures:** `NPS: Promoters`, `NPS: Passives`, `NPS: Detractors` + + + Bar chart comparing 113 promoters, 76 passives and 51 detractors + + +### Put them on a dashboard + +Go to **Analyze → Analysis → Dashboards**, click **Create dashboard**, then **Add charts** and pick the three charts +you just built. Drag and resize the widgets into the layout you want; a dashboard-level **Date range** filter sits +above the grid. + + + NPS Health dashboard combining the overall score, the trend, the promoter split and comment sentiment + + +## Use case 2: Why the score moved + +The NPS template asks a follow-up question - "can you describe the reason(s) for your rating?" - and those answers +land as `text` records. Enrichment scores each one for sentiment as it arrives, so you can chart the *reasons* +without reading them all. + +- **Chart type:** Pie Chart +- **Measure:** `Responses` +- **Filter data:** `Question` equals your follow-up question +- **Group data → Group By:** `Sentiment` + + + Pie chart breaking 240 NPS comments down by sentiment + + +The filter matters. Without it the chart also counts the NPS number records, which have no sentiment, and roughly +half of your pie becomes a "Not enriched" slice. + +From here, open **Analyze → Feedback Data → Feedback Records**, filter to the negative comments and read the +verbatims behind the slice. See [Enrichment](/unify-feedback/enrichment) for what else is scored. + +## Use case 3: One dashboard across surveys + +This is where records earn their keep. Add a second survey - a CSAT survey after checkout or a support +interaction - and connect it as a **second source into the same dataset**. Both surveys now produce records on the +same time axis, so they can share a chart. + +- **Chart type:** Line Chart +- **Measures:** `NPS: Score`, `CSAT: Score` +- **Add time-based grouping:** Collected At, Month, This year + + + Line chart plotting NPS: Score and CSAT: Score by month on the same axis + + +Both measures are percentages of a kind, so they read sensibly on one axis: `NPS: Score` runs from −100 to 100 and +`CSAT: Score` is the share of responses rated 4 or 5 on the 1-5 scale. When relationship-level NPS and +transactional CSAT diverge, that gap is usually the interesting part. + +## When a chart looks empty + + + + The workspace has no dataset. Create one under **Settings → Organization → Datasets** and add this workspace under + **Workspace access**. + + + + No source is writing into it yet, or the source was created with **Import historical responses** switched off. + Check **Analyze → Feedback Data → Feedback Sources**, and confirm the banner under the table names the dataset you + expect. + + + + Widen the **Date Range**. It defaults to **Last 30 days**, and a trend chart at **Month** granularity over 30 days + has almost nothing to draw. + + + + You are counting records that carry no value for that dimension - usually numeric records in a chart grouped by + sentiment. Add a **Filter data** condition on `Question` or field type to narrow the chart to the records the + dimension applies to. + + + +## Next steps + + + + Chart types, the AI builder, and dashboard permissions. + + + CSV uploads, API ingestion, and field mapping. + + + Sentiment, emotions, and language consolidation. + + + Cluster open text into themes. + + diff --git a/docs/unify-feedback/feedback-datasets.mdx b/docs/unify-feedback/feedback-datasets.mdx index 516a7f567727..5349ec6ee563 100644 --- a/docs/unify-feedback/feedback-datasets.mdx +++ b/docs/unify-feedback/feedback-datasets.mdx @@ -18,14 +18,31 @@ Create one dataset per logically separate group of feedback. Common patterns: Datasets live at the **organization** level but are exposed to **workspaces** through an access list. Each workspace can **only access one dataset.** -Manage dataset access from **Settings → Organization → Feedback Datasets**: +Manage dataset access from **Settings → Organization → Datasets**: - Create new datasets - Rename or archive datasets - Add or remove workspace access +- Delete all records in a dataset Only **Owners** and **Managers** can manage datasets. Workspace members see the datasets their workspace has access to under **Analyze**. ## Archiving Archiving a dataset hides it from default views but does not delete its records. Use it for one-off programs that have ended. + +## Delete all records + +**Delete all records** empties a dataset while keeping the dataset itself. Open **Settings → Organization → Datasets**, click **Manage** on the dataset, then click **Delete all records**. + + + This cannot be undone, and it is not limited to your workspace. It permanently deletes every feedback record in the dataset — including its AI enrichment and the topics generated from it — for **every workspace the dataset is shared with**. + + +The dataset and its sources are kept, so new feedback keeps arriving. Topics are generated again once new feedback comes in, and charts built on the dataset are empty until then. + +To confirm, type the dataset's exact name. Only **Owners** and **Managers** can do this; everyone else sees the button disabled. + +Deletion runs in the background, so records may still show up for a few minutes after you confirm. + +This is what separates it from archiving: archiving keeps the records and hides the dataset, deleting keeps the dataset and removes the records. diff --git a/docs/unify-feedback/feedback-records.mdx b/docs/unify-feedback/feedback-records.mdx index 34108160d01a..b2fe5541746e 100644 --- a/docs/unify-feedback/feedback-records.mdx +++ b/docs/unify-feedback/feedback-records.mdx @@ -35,7 +35,7 @@ The right `value_*` field is set based on `field_type`. For example a `nps` fiel ## Viewing and managing records -Inside a workspace, navigate to **Unify → Feedback Records**. You'll see the latest records across every dataset the workspace has access to, sorted by `collected_at`. +Inside a workspace, navigate to **Analyze → Feedback Data → Feedback Records**. You'll see the latest records across every dataset the workspace has access to, sorted by `collected_at`. From the table you can: diff --git a/docs/unify-feedback/feedback-sources.mdx b/docs/unify-feedback/feedback-sources.mdx index 6c3a9d5c80cb..2791a920a797 100644 --- a/docs/unify-feedback/feedback-sources.mdx +++ b/docs/unify-feedback/feedback-sources.mdx @@ -4,7 +4,7 @@ description: "Sources that bring feedback data into a Feedback Dataset." icon: "plug" --- -A **Source** defines how external data is mapped into Feedback Records inside a Feedback Dataset. Manage them from **Unify → Sources**. +A **Source** defines how external data is mapped into Feedback Records inside a Feedback Dataset. Manage them from **Analyze → Feedback Data → Feedback Sources**. ## Source types diff --git a/docs/unify-feedback/topics-subtopics.mdx b/docs/unify-feedback/topics-subtopics.mdx index e59938e1bd54..7e278b3e58d5 100644 --- a/docs/unify-feedback/topics-subtopics.mdx +++ b/docs/unify-feedback/topics-subtopics.mdx @@ -12,7 +12,7 @@ Open-text feedback ("Why did you give this score?", support tickets, app reviews ## How it works -1. Pick a Feedback Dataset under **Unify → Topics & Subtopics**. +1. Pick a Feedback Dataset under **Analyze → Feedback Data → Topics & Subtopics**. 2. Formbricks scans `value_text` across the dataset and proposes a set of **Topics** (broad categories) and **Subtopics** (specific themes within a topic). 3. Each record can be assigned to one Topic and one Subtopic.