From 8ef6dc0a0714423ce5bd6a5a31cd88198e2a02c0 Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:41:11 +0530 Subject: [PATCH] fix: global error handling js package (#2553) --- packages/js/src/app.ts | 101 +++++++++++++++++++++------------- packages/js/src/website.ts | 110 +++++++++++++++++++++++-------------- 2 files changed, 132 insertions(+), 79 deletions(-) diff --git a/packages/js/src/app.ts b/packages/js/src/app.ts index e824321bee..ac54e1ea59 100644 --- a/packages/js/src/app.ts +++ b/packages/js/src/app.ts @@ -1,37 +1,57 @@ import { TFormbricksApp } from "@formbricks/js-core/app"; import { TFormbricksWebsite } from "@formbricks/js-core/website"; +import { Result, wrapThrowsAsync } from "../../types/errorHandlers"; + declare global { interface Window { formbricks: TFormbricksApp | TFormbricksWebsite; } } -let sdkLoadingPromise: Promise | null = null; -let isErrorLoadingSdk = false; - -async function loadSDK(apiHost: string) { +// load the sdk, return the result +async function loadFormbricksAppSDK(apiHost: string): Promise> { if (!window.formbricks) { const res = await fetch(`${apiHost}/api/packages/app`); - if (!res.ok) throw new Error("Failed to load Formbricks App SDK"); + + // 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); - return new Promise((resolve, reject) => { - const checkInterval = setInterval(() => { - if (window.formbricks) { + const getFormbricks = async () => + new Promise((resolve, reject) => { + const checkInterval = setInterval(() => { + if (window.formbricks) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + + setTimeout(() => { clearInterval(checkInterval); - resolve(); - } - }, 100); - setTimeout(() => { - clearInterval(checkInterval); - reject(new Error("Formbricks SDK loading timed out")); - }, 10000); - }); + 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 = { @@ -41,31 +61,34 @@ type FormbricksAppMethods = { const formbricksProxyHandler: ProxyHandler = { get(_target, prop, _receiver) { return async (...args: any[]) => { - if (!window.formbricks && !sdkLoadingPromise && !isErrorLoadingSdk) { - const { apiHost } = args[0]; - sdkLoadingPromise = loadSDK(apiHost).catch((error) => { - console.error(`🧱 Formbricks - Error loading SDK: ${error}`); - sdkLoadingPromise = null; - isErrorLoadingSdk = true; - return; - }); - } - - if (isErrorLoadingSdk) { - return; - } - - if (sdkLoadingPromise) { - await sdkLoadingPromise; - } - if (!window.formbricks) { - throw new Error("Formbricks App SDK is not available"); + 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 (typeof window.formbricks[prop as FormbricksAppMethods] !== "function") { - console.error(`🧱 Formbricks App SDK does not support method ${String(prop)}`); + 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; } @@ -73,8 +96,8 @@ const formbricksProxyHandler: ProxyHandler = { // @ts-expect-error return (window.formbricks[prop as FormbricksAppMethods] as Function)(...args); } catch (error) { - console.error(error); - throw error; + console.error(`Something went wrong: ${error}`); + return; } }; }, diff --git a/packages/js/src/website.ts b/packages/js/src/website.ts index 156a3b2c1f..9def31e577 100644 --- a/packages/js/src/website.ts +++ b/packages/js/src/website.ts @@ -1,30 +1,57 @@ +import { TFormbricksApp } from "@formbricks/js-core/app"; import { TFormbricksWebsite } from "@formbricks/js-core/website"; -let sdkLoadingPromise: Promise | null = null; -let isErrorLoadingSdk = false; +import { Result, wrapThrowsAsync } from "../../types/errorHandlers"; -async function loadSDK(apiHost: string) { +declare global { + interface Window { + formbricks: TFormbricksApp | TFormbricksWebsite; + } +} + +// load the sdk, return the result +async function loadFormbricksWebsiteSDK(apiHost: string): Promise> { if (!window.formbricks) { const res = await fetch(`${apiHost}/api/packages/website`); - if (!res.ok) throw new Error("Failed to load Formbricks Website SDK"); + + // 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); - return new Promise((resolve, reject) => { - const checkInterval = setInterval(() => { - if (window.formbricks) { + const getFormbricks = async () => + new Promise((resolve, reject) => { + const checkInterval = setInterval(() => { + if (window.formbricks) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + + setTimeout(() => { clearInterval(checkInterval); - resolve(); - } - }, 100); - setTimeout(() => { - clearInterval(checkInterval); - reject(new Error("Formbricks SDK loading timed out")); - }, 10000); - }); + 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 = { @@ -34,42 +61,45 @@ type FormbricksWebsiteMethods = { const formbricksProxyHandler: ProxyHandler = { get(_target, prop, _receiver) { return async (...args: any[]) => { - if (!window.formbricks && !sdkLoadingPromise && !isErrorLoadingSdk) { - const { apiHost } = args[0]; - sdkLoadingPromise = loadSDK(apiHost).catch((error) => { - console.error(`🧱 Formbricks - Error loading SDK: ${error}`); - sdkLoadingPromise = null; - isErrorLoadingSdk = true; - return; - }); - } - - if (isErrorLoadingSdk) { - return; - } - - if (sdkLoadingPromise) { - await sdkLoadingPromise; - } - if (!window.formbricks) { - throw new Error("Formbricks Website SDK is not available"); + 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 (typeof window.formbricks[prop as FormbricksWebsiteMethods] !== "function") { - console.error(`🧱 Formbricks Website SDK does not support method ${String(prop)}`); + 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(error); - throw error; + console.error(`🧱 Formbricks - Global error: Something went wrong: ${error}`); + return; } }; }, }; -const formbricksWebsite: TFormbricksWebsite = new Proxy({} as TFormbricksWebsite, formbricksProxyHandler); -export default formbricksWebsite; +const formbricksApp: TFormbricksWebsite = new Proxy({} as TFormbricksWebsite, formbricksProxyHandler); +export default formbricksApp;