mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Merge branch 'main' into fix/product-settings-form
This commit is contained in:
@@ -20,7 +20,9 @@ Formbricks v2.0 comes with huge features such as Multi-Language Surveys and Adva
|
||||
and
|
||||
|
||||
<Note>
|
||||
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/)
|
||||
</Note>
|
||||
|
||||
### Steps to Migrate
|
||||
@@ -35,7 +37,7 @@ To run all these steps, please navigate to the `formbricks` folder where your `d
|
||||
<CodeGroup title="Backup Postgres">
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -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.
|
||||
</Note>
|
||||
|
||||
2. Stop the running Formbricks instance & remove the related containers:
|
||||
2. Pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Stop the running Formbricks instance & remove the related containers:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
@@ -63,7 +77,7 @@ docker-compose down
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Restarting the containers will automatically pull the latest version of Formbricks:
|
||||
4. Restarting the containers with the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Restart the containers">
|
||||
@@ -75,7 +89,7 @@ docker-compose up -d
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Now let's migrate the data to the latest schema:
|
||||
5. Now let's migrate the data to the latest schema:
|
||||
|
||||
<Note>To find your Docker Network name for your Postgres Database, find it using `docker network ls`</Note>
|
||||
|
||||
@@ -83,6 +97,7 @@ docker-compose up -d
|
||||
<CodeGroup title="Migrate the data">
|
||||
|
||||
```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
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export const POST = async (request: Request): Promise<Response> => {
|
||||
url: responseInput?.meta?.url,
|
||||
userAgent: {
|
||||
browser: agent?.browser.name,
|
||||
device: agent?.device.type,
|
||||
device: agent?.device.type || "desktop",
|
||||
os: agent?.os.name,
|
||||
},
|
||||
country: country,
|
||||
|
||||
@@ -82,7 +82,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
url: responseInput?.meta?.url,
|
||||
userAgent: {
|
||||
browser: agent?.browser.name,
|
||||
device: agent?.device.type,
|
||||
device: agent?.device.type || "desktop",
|
||||
os: agent?.os.name,
|
||||
},
|
||||
country: country,
|
||||
|
||||
@@ -114,7 +114,7 @@ export const GET = async (
|
||||
const noCodeActionClasses = actionClasses.filter((actionClass) => 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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<Result<void>> => {
|
||||
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<void>((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<TFormbricksApp> = {
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as formbricksApp } from "./app";
|
||||
export { default as formbricksWebsite } from "./website";
|
||||
38
packages/js/src/methodQueue.ts
Normal file
38
packages/js/src/methodQueue.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Simple queue for formbricks methods
|
||||
|
||||
export class MethodQueue {
|
||||
private queue: (() => Promise<void>)[] = [];
|
||||
private isExecuting = false;
|
||||
|
||||
add = (method: () => Promise<void>) => {
|
||||
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 = [];
|
||||
};
|
||||
}
|
||||
122
packages/js/src/shared/loadFormbricks.ts
Normal file
122
packages/js/src/shared/loadFormbricks.ts
Normal file
@@ -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<Result<void>> => {
|
||||
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<void>((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;
|
||||
}
|
||||
};
|
||||
@@ -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<Result<void>> => {
|
||||
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<void>((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<TFormbricksWebsite> = {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user