fix: formbricks init error (#2633)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Anshuman Pandey
2024-05-22 17:37:17 +05:30
committed by GitHub
parent 732b8b599f
commit 48859facf4
8 changed files with 183 additions and 198 deletions

View File

@@ -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;
};

View File

@@ -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,

View File

@@ -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": {

View File

@@ -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);
},
};

View File

@@ -1,2 +0,0 @@
export { default as formbricksApp } from "./app";
export { default as formbricksWebsite } from "./website";

View 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 = [];
};
}

View 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;
}
};

View File

@@ -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;