diff --git a/apps/docs/app/self-hosting/migration-guide/page.mdx b/apps/docs/app/self-hosting/migration-guide/page.mdx index 53e6473e14..1bcbcb0244 100644 --- a/apps/docs/app/self-hosting/migration-guide/page.mdx +++ b/apps/docs/app/self-hosting/migration-guide/page.mdx @@ -20,7 +20,9 @@ Formbricks v2.0 comes with huge features such as Multi-Language Surveys and Adva and - If you've used the Formbricks Enterprise Edition with a free beta license key, your instance will be downgraded to the Community Edition 2.0. You find all license details on the [license page.](/self-hosting/license/) + If you've used the Formbricks Enterprise Edition with a free beta license key, your instance will be + downgraded to the Community Edition 2.0. You find all license details on the [license + page.](/self-hosting/license/) ### Steps to Migrate @@ -35,7 +37,7 @@ To run all these steps, please navigate to the `formbricks` folder where your `d ```bash -docker exec formbricks-quickstart-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.0_$(date +%Y%m%d_%H%M%S).dump +docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.0_$(date +%Y%m%d_%H%M%S).dump ``` @@ -51,7 +53,19 @@ docker exec formbricks-quickstart-postgres-1 pg_dump -Fc -U postgres -d formbric restore scenario you will need to use `psql` then with an empty `formbricks` database. -2. Stop the running Formbricks instance & remove the related containers: +2. Pull the latest version of Formbricks: + + + + +```bash +docker-compose pull +``` + + + + +3. Stop the running Formbricks instance & remove the related containers: @@ -63,7 +77,7 @@ docker-compose down -3. Restarting the containers will automatically pull the latest version of Formbricks: +4. Restarting the containers with the latest version of Formbricks: @@ -75,7 +89,7 @@ docker-compose up -d -4. Now let's migrate the data to the latest schema: +5. Now let's migrate the data to the latest schema: To find your Docker Network name for your Postgres Database, find it using `docker network ls` @@ -83,6 +97,7 @@ docker-compose up -d ```bash +docker pull ghcr.io/formbricks/data-migrations:latest && \ docker run --rm \ --network=formbricks_default \ -e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \ @@ -95,7 +110,7 @@ docker run --rm \ The above command will migrate your data to the latest schema. This is a crucial step to migrate your existing data to the new structure. Only if the script runs successful, changes are made to the database. The script can safely run multiple times. -5. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before. +6. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before. ### App Surveys with @formbricks/js diff --git a/apps/web/app/(app)/components/FormbricksClient.tsx b/apps/web/app/(app)/components/FormbricksClient.tsx index 18832a0712..e516e7cee3 100644 --- a/apps/web/app/(app)/components/FormbricksClient.tsx +++ b/apps/web/app/(app)/components/FormbricksClient.tsx @@ -2,7 +2,7 @@ import { formbricksEnabled } from "@/app/lib/formbricks"; import { usePathname, useSearchParams } from "next/navigation"; -import { useEffect } from "react"; +import { useCallback, useEffect } from "react"; import formbricks from "@formbricks/js/app"; import { env } from "@formbricks/lib/env"; @@ -15,22 +15,23 @@ export const FormbricksClient = ({ session }) => { const pathname = usePathname(); const searchParams = useSearchParams(); - useEffect(() => { - if (formbricksEnabled && session?.user && formbricks) { - formbricks.init({ - environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "", - apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "", - userId: session.user.id, - }); - formbricks.setEmail(session.user.email); - } - }, [session]); + const initializeFormbricksAndSetupRouteChanges = useCallback(async () => { + formbricks.init({ + environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "", + apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "", + userId: session.user.id, + }); + formbricks.setEmail(session.user.email); + + formbricks.registerRouteChange(); + }, [session.user.email, session.user.id]); useEffect(() => { - if (formbricksEnabled && formbricks) { - formbricks?.registerRouteChange(); + if (formbricksEnabled && session?.user && formbricks) { + initializeFormbricksAndSetupRouteChanges(); } - }, [pathname, searchParams]); + }, [session, pathname, searchParams, initializeFormbricksAndSetupRouteChanges]); + return null; }; diff --git a/apps/web/app/api/v1/(legacy)/client/responses/route.ts b/apps/web/app/api/v1/(legacy)/client/responses/route.ts index d4e55962be..810ffca342 100644 --- a/apps/web/app/api/v1/(legacy)/client/responses/route.ts +++ b/apps/web/app/api/v1/(legacy)/client/responses/route.ts @@ -59,7 +59,7 @@ export const POST = async (request: Request): Promise => { url: responseInput?.meta?.url, userAgent: { browser: agent?.browser.name, - device: agent?.device.type, + device: agent?.device.type || "desktop", os: agent?.os.name, }, country: country, diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts index df724a99a5..2f93f8e398 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts @@ -82,7 +82,7 @@ export const POST = async (request: Request, context: Context): Promise actionClass.type === "noCode"); // Define 'transformedSurveys' which can be an array of either TLegacySurvey or TSurvey. - let transformedSurveys: TLegacySurvey[] | TSurvey[] = surveys; + let transformedSurveys: TLegacySurvey[] | TSurvey[] = filteredSurveys; let state: TJsWebsiteStateSync | TJsWebsiteLegacyStateSync = { surveys: !isInAppSurveyLimitReached ? transformedSurveys : [], actionClasses, diff --git a/packages/js/package.json b/packages/js/package.json index e6f2319f93..7b5766f614 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,7 +1,7 @@ { "name": "@formbricks/js", "license": "MIT", - "version": "2.0.0", + "version": "2.0.1", "description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.", "homepage": "https://formbricks.com", "repository": { diff --git a/packages/js/src/app.ts b/packages/js/src/app.ts index 9bd96d69b7..131f87619d 100644 --- a/packages/js/src/app.ts +++ b/packages/js/src/app.ts @@ -1,7 +1,7 @@ import { TFormbricksApp } from "@formbricks/js-core/app"; import { TFormbricksWebsite } from "@formbricks/js-core/website"; -import { Result, wrapThrowsAsync } from "../../types/errorHandlers"; +import { loadFormbricksToProxy } from "./shared/loadFormbricks"; declare global { interface Window { @@ -9,97 +9,9 @@ declare global { } } -// load the sdk, return the result -const loadFormbricksAppSDK = async (apiHost: string): Promise> => { - if (!window.formbricks) { - const res = await fetch(`${apiHost}/api/packages/app`); - - // failed to fetch the app package - if (!res.ok) { - return { ok: false, error: new Error("Failed to load Formbricks App SDK") }; - } - - const sdkScript = await res.text(); - const scriptTag = document.createElement("script"); - scriptTag.innerHTML = sdkScript; - document.head.appendChild(scriptTag); - - const getFormbricks = async () => - new Promise((resolve, reject) => { - const checkInterval = setInterval(() => { - if (window.formbricks) { - clearInterval(checkInterval); - resolve(); - } - }, 100); - - setTimeout(() => { - clearInterval(checkInterval); - reject(new Error("Formbricks App SDK loading timed out")); - }, 10000); - }); - - try { - await getFormbricks(); - return { ok: true, data: undefined }; - } catch (error: any) { - // formbricks loading failed, return the error - return { - ok: false, - error: new Error(error.message ?? "Failed to load Formbricks App SDK"), - }; - } - } - - return { ok: true, data: undefined }; -}; - -type FormbricksAppMethods = { - [K in keyof TFormbricksApp]: TFormbricksApp[K] extends Function ? K : never; -}[keyof TFormbricksApp]; - const formbricksProxyHandler: ProxyHandler = { get(_target, prop, _receiver) { - return async (...args: any[]) => { - if (!window.formbricks) { - if (prop !== "init") { - console.error( - "🧱 Formbricks - Global error: You need to call formbricks.init before calling any other method" - ); - return; - } - - // still need to check if the apiHost is passed - if (!args[0]) { - console.error("🧱 Formbricks - Global error: You need to pass the apiHost as the first argument"); - return; - } - - const { apiHost } = args[0]; - const loadSDKResult = await wrapThrowsAsync(loadFormbricksAppSDK)(apiHost); - - if (!loadSDKResult.ok) { - console.error(`🧱 Formbricks - Global error: ${loadSDKResult.error.message}`); - return; - } - } - - // @ts-expect-error - if (window.formbricks && typeof window.formbricks[prop as FormbricksAppMethods] !== "function") { - console.error( - `🧱 Formbricks - Global error: Formbricks App SDK does not support method ${String(prop)}` - ); - return; - } - - try { - // @ts-expect-error - return (window.formbricks[prop as FormbricksAppMethods] as Function)(...args); - } catch (error) { - console.error(`Something went wrong: ${error}`); - return; - } - }; + return (...args: any[]) => loadFormbricksToProxy(prop as string, "app", ...args); }, }; diff --git a/packages/js/src/index.ts b/packages/js/src/index.ts deleted file mode 100644 index 2c8b92f2d2..0000000000 --- a/packages/js/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as formbricksApp } from "./app"; -export { default as formbricksWebsite } from "./website"; diff --git a/packages/js/src/methodQueue.ts b/packages/js/src/methodQueue.ts new file mode 100644 index 0000000000..5df9102ebc --- /dev/null +++ b/packages/js/src/methodQueue.ts @@ -0,0 +1,38 @@ +// Simple queue for formbricks methods + +export class MethodQueue { + private queue: (() => Promise)[] = []; + private isExecuting = false; + + add = (method: () => Promise) => { + this.queue.push(method); + this.run(); + }; + + private runNext = async () => { + if (this.isExecuting) return; + + const method = this.queue.shift(); + if (method) { + this.isExecuting = true; + try { + await method(); + } finally { + this.isExecuting = false; + if (this.queue.length > 0) { + this.runNext(); + } + } + } + }; + + run = async () => { + if (!this.isExecuting && this.queue.length > 0) { + await this.runNext(); + } + }; + + clear = () => { + this.queue = []; + }; +} diff --git a/packages/js/src/shared/loadFormbricks.ts b/packages/js/src/shared/loadFormbricks.ts new file mode 100644 index 0000000000..b9250a8584 --- /dev/null +++ b/packages/js/src/shared/loadFormbricks.ts @@ -0,0 +1,122 @@ +import { Result, wrapThrowsAsync } from "../../../types/errorHandlers"; +import { MethodQueue } from "../methodQueue"; + +let isInitializing = false; +let isInitialized = false; +const methodQueue = new MethodQueue(); + +// Load the SDK, return the result +const loadFormbricksSDK = async (apiHost: string, sdkType: "app" | "website"): Promise> => { + if (!window.formbricks) { + const res = await fetch(`${apiHost}/api/packages/${sdkType}`); + + // Failed to fetch the app package + if (!res.ok) { + return { ok: false, error: new Error(`Failed to load Formbricks ${sdkType} SDK`) }; + } + + const sdkScript = await res.text(); + const scriptTag = document.createElement("script"); + scriptTag.innerHTML = sdkScript; + document.head.appendChild(scriptTag); + + const getFormbricks = async () => + new Promise((resolve, reject) => { + const checkInterval = setInterval(() => { + if (window.formbricks) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + + setTimeout(() => { + clearInterval(checkInterval); + reject(new Error(`Formbricks ${sdkType} SDK loading timed out`)); + }, 10000); + }); + + try { + await getFormbricks(); + return { ok: true, data: undefined }; + } catch (error: any) { + return { + ok: false, + error: new Error(error.message ?? `Failed to load Formbricks ${sdkType} SDK`), + }; + } + } + + return { ok: true, data: undefined }; +}; + +// TODO: @pandeymangg - Fix these types +// type FormbricksAppMethods = { +// [K in keyof TFormbricksApp]: TFormbricksApp[K] extends Function ? K : never; +// }[keyof TFormbricksApp]; + +// type FormbricksWebsiteMethods = { +// [K in keyof TFormbricksWebsite]: TFormbricksWebsite[K] extends Function ? K : never; +// }[keyof TFormbricksWebsite]; + +export const loadFormbricksToProxy = async (prop: string, sdkType: "app" | "website", ...args: any[]) => { + const executeMethod = async () => { + try { + // @ts-expect-error + return await (window.formbricks[prop] as Function)(...args); + } catch (error) { + console.error(`🧱 Formbricks - Global error: ${error}`); + throw error; + } + }; + + if (!isInitialized) { + if (isInitializing) { + methodQueue.add(executeMethod); + } else { + if (prop === "init") { + isInitializing = true; + + const initialize = async () => { + const { apiHost } = args[0]; + const loadSDKResult = await wrapThrowsAsync(loadFormbricksSDK)(apiHost, sdkType); + + if (!loadSDKResult.ok) { + isInitializing = false; + console.error(`🧱 Formbricks - Global error: ${loadSDKResult.error.message}`); + return; + } + + try { + const result = await (window.formbricks[prop] as Function)(...args); + isInitialized = true; + isInitializing = false; + + return result; + } catch (error) { + isInitializing = false; + console.error(`🧱 Formbricks - Global error: ${error}`); + throw error; + } + }; + + methodQueue.add(initialize); + } else { + console.error( + "🧱 Formbricks - Global error: You need to call formbricks.init before calling any other method" + ); + return; + } + } + } else { + // @ts-expect-error + if (window.formbricks && typeof window.formbricks[prop] !== "function") { + console.error( + `🧱 Formbricks - Global error: Formbricks ${sdkType} SDK does not support method ${String(prop)}` + ); + return; + } + + methodQueue.add(executeMethod); + return; + } +}; diff --git a/packages/js/src/website.ts b/packages/js/src/website.ts index ec41c93dc0..2b7e035d90 100644 --- a/packages/js/src/website.ts +++ b/packages/js/src/website.ts @@ -1,7 +1,7 @@ import { TFormbricksApp } from "@formbricks/js-core/app"; import { TFormbricksWebsite } from "@formbricks/js-core/website"; -import { Result, wrapThrowsAsync } from "../../types/errorHandlers"; +import { loadFormbricksToProxy } from "./shared/loadFormbricks"; declare global { interface Window { @@ -9,97 +9,11 @@ declare global { } } -// load the sdk, return the result -const loadFormbricksWebsiteSDK = async (apiHost: string): Promise> => { - if (!window.formbricks) { - const res = await fetch(`${apiHost}/api/packages/website`); - - // failed to fetch the app package - if (!res.ok) { - return { ok: false, error: new Error("Failed to load Formbricks Website SDK") }; - } - - const sdkScript = await res.text(); - const scriptTag = document.createElement("script"); - scriptTag.innerHTML = sdkScript; - document.head.appendChild(scriptTag); - - const getFormbricks = async () => - new Promise((resolve, reject) => { - const checkInterval = setInterval(() => { - if (window.formbricks) { - clearInterval(checkInterval); - resolve(); - } - }, 100); - - setTimeout(() => { - clearInterval(checkInterval); - reject(new Error("Formbricks Website SDK loading timed out")); - }, 10000); - }); - - try { - await getFormbricks(); - return { ok: true, data: undefined }; - } catch (error: any) { - // formbricks loading failed, return the error - return { - ok: false, - error: new Error(error.message ?? "Failed to load Formbricks Website SDK"), - }; - } - } - - return { ok: true, data: undefined }; -}; - -type FormbricksWebsiteMethods = { - [K in keyof TFormbricksWebsite]: TFormbricksWebsite[K] extends Function ? K : never; -}[keyof TFormbricksWebsite]; - const formbricksProxyHandler: ProxyHandler = { get(_target, prop, _receiver) { - return async (...args: any[]) => { - if (!window.formbricks) { - if (prop !== "init") { - console.error( - "🧱 Formbricks - Global error: You need to call formbricks.init before calling any other method" - ); - return; - } - - // still need to check if the apiHost is passed - if (!args[0]) { - console.error("🧱 Formbricks - Global error: You need to pass the apiHost as the first argument"); - return; - } - - const { apiHost } = args[0]; - const loadSDKResult = await wrapThrowsAsync(loadFormbricksWebsiteSDK)(apiHost); - - if (!loadSDKResult.ok) { - console.error(`🧱 Formbricks - Global error: ${loadSDKResult.error.message}`); - return; - } - } - - if (window.formbricks && typeof window.formbricks[prop as FormbricksWebsiteMethods] !== "function") { - console.error( - `🧱 Formbricks - Global error: Formbricks Website SDK does not support method ${String(prop)}` - ); - return; - } - - try { - return (window.formbricks[prop as FormbricksWebsiteMethods] as Function)(...args); - } catch (error) { - console.error(`🧱 Formbricks - Global error: Something went wrong: ${error}`); - return; - } - }; + return (...args: any[]) => loadFormbricksToProxy(prop as string, "website", ...args); }, }; -const formbricksApp: TFormbricksWebsite = new Proxy({} as TFormbricksWebsite, formbricksProxyHandler); -export default formbricksApp; +const formbricksWebsite: TFormbricksWebsite = new Proxy({} as TFormbricksWebsite, formbricksProxyHandler); +export default formbricksWebsite;