mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-04 11:30:38 -05:00
feat: sync endpoint error handling (#2132)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -46,7 +46,10 @@ export class CommandQueue {
|
||||
if (currentItem.checkInitialized) {
|
||||
const initResult = checkInitialized();
|
||||
|
||||
if (initResult && initResult.ok !== true) errorHandler.handle(initResult.error);
|
||||
if (initResult && initResult.ok !== true) {
|
||||
errorHandler.handle(initResult.error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const executeCommand = async () => {
|
||||
|
||||
@@ -25,11 +25,12 @@ export class Config {
|
||||
|
||||
public update(newConfig: TJsConfigUpdateInput): void {
|
||||
if (newConfig) {
|
||||
const expiresAt = new Date(new Date().getTime() + 2 * 60000); // 2 minutes from now
|
||||
const expiresAt = new Date(new Date().getTime() + 2 * 60000); // 2 minutes in the future
|
||||
|
||||
this.config = {
|
||||
...this.config,
|
||||
...newConfig,
|
||||
status: newConfig.status || "success",
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TJsConfig, TJsConfigInput } from "@formbricks/types/js";
|
||||
import { TPersonAttributes } from "@formbricks/types/people";
|
||||
|
||||
import { trackAction } from "./actions";
|
||||
import { Config } from "./config";
|
||||
import { Config, LOCAL_STORAGE_KEY } from "./config";
|
||||
import {
|
||||
ErrorHandler,
|
||||
MissingFieldError,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Result,
|
||||
err,
|
||||
okVoid,
|
||||
wrapThrows,
|
||||
} from "./errors";
|
||||
import { addCleanupEventListeners, addEventListeners, removeAllEventListeners } from "./eventListeners";
|
||||
import { Logger } from "./logger";
|
||||
@@ -19,23 +20,46 @@ import { checkPageUrl } from "./noCodeActions";
|
||||
import { updatePersonAttributes } from "./person";
|
||||
import { sync } from "./sync";
|
||||
import { getIsDebug } from "./utils";
|
||||
import { addWidgetContainer, closeSurvey } from "./widget";
|
||||
import { addWidgetContainer, removeWidgetContainer, setIsSurveyRunning } from "./widget";
|
||||
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
let isInitialized = false;
|
||||
|
||||
export const setIsInitialized = (value: boolean) => {
|
||||
isInitialized = value;
|
||||
};
|
||||
|
||||
export const initialize = async (
|
||||
c: TJsConfigInput
|
||||
): Promise<Result<void, MissingFieldError | NetworkError | MissingPersonError>> => {
|
||||
if (getIsDebug()) {
|
||||
logger.configure({ logLevel: "debug" });
|
||||
}
|
||||
|
||||
if (isInitialized) {
|
||||
logger.debug("Already initialized, skipping initialization.");
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
if (getIsDebug()) {
|
||||
logger.configure({ logLevel: "debug" });
|
||||
let existingConfig: TJsConfig | undefined;
|
||||
try {
|
||||
existingConfig = config.get();
|
||||
logger.debug("Found existing configuration.");
|
||||
} catch (e) {
|
||||
logger.debug("No existing configuration found.");
|
||||
}
|
||||
|
||||
// formbricks is in error state, skip initialization
|
||||
if (existingConfig?.status === "error") {
|
||||
logger.debug("Formbricks was set to an error state.");
|
||||
if (existingConfig?.expiresAt && new Date(existingConfig.expiresAt) > new Date()) {
|
||||
logger.debug("Error state is not expired, skipping initialization");
|
||||
return okVoid();
|
||||
} else {
|
||||
logger.debug("Error state is expired. Continue with initialization.");
|
||||
}
|
||||
}
|
||||
|
||||
ErrorHandler.getInstance().printStatus();
|
||||
@@ -81,13 +105,6 @@ export const initialize = async (
|
||||
updatedAttributes = res.value;
|
||||
}
|
||||
|
||||
let existingConfig: TJsConfig | undefined;
|
||||
try {
|
||||
existingConfig = config.get();
|
||||
} catch (e) {
|
||||
logger.debug("No existing configuration found.");
|
||||
}
|
||||
|
||||
if (
|
||||
existingConfig &&
|
||||
existingConfig.state &&
|
||||
@@ -96,29 +113,39 @@ export const initialize = async (
|
||||
existingConfig.userId === c.userId &&
|
||||
existingConfig.expiresAt // only accept config when they follow new config version with expiresAt
|
||||
) {
|
||||
logger.debug("Found existing configuration.");
|
||||
logger.debug("Configuration fits init parameters.");
|
||||
if (existingConfig.expiresAt < new Date()) {
|
||||
logger.debug("Configuration expired.");
|
||||
|
||||
await sync({
|
||||
apiHost: c.apiHost,
|
||||
environmentId: c.environmentId,
|
||||
userId: c.userId,
|
||||
});
|
||||
try {
|
||||
await sync({
|
||||
apiHost: c.apiHost,
|
||||
environmentId: c.environmentId,
|
||||
userId: c.userId,
|
||||
});
|
||||
} catch (e) {
|
||||
putFormbricksInErrorState();
|
||||
}
|
||||
} else {
|
||||
logger.debug("Configuration not expired. Extending expiration.");
|
||||
config.update(existingConfig);
|
||||
}
|
||||
} else {
|
||||
logger.debug("No valid configuration found or it has been expired. Creating new config.");
|
||||
logger.debug(
|
||||
"No valid configuration found or it has been expired. Resetting config and creating new one."
|
||||
);
|
||||
config.resetConfig();
|
||||
logger.debug("Syncing.");
|
||||
|
||||
await sync({
|
||||
apiHost: c.apiHost,
|
||||
environmentId: c.environmentId,
|
||||
userId: c.userId,
|
||||
});
|
||||
|
||||
try {
|
||||
await sync({
|
||||
apiHost: c.apiHost,
|
||||
environmentId: c.environmentId,
|
||||
userId: c.userId,
|
||||
});
|
||||
} catch (e) {
|
||||
handleErrorOnFirstInit();
|
||||
}
|
||||
// and track the new session event
|
||||
await trackAction("New Session");
|
||||
}
|
||||
@@ -140,7 +167,7 @@ export const initialize = async (
|
||||
addEventListeners();
|
||||
addCleanupEventListeners();
|
||||
|
||||
isInitialized = true;
|
||||
setIsInitialized(true);
|
||||
logger.debug("Initialized");
|
||||
|
||||
// check page url if initialized after page load
|
||||
@@ -149,6 +176,17 @@ export const initialize = async (
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
const handleErrorOnFirstInit = () => {
|
||||
// put formbricks in error state (by creating a new config) and throw error
|
||||
const initialErrorConfig: Partial<TJsConfig> = {
|
||||
status: "error",
|
||||
expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future
|
||||
};
|
||||
// can't use config.update here because the config is not yet initialized
|
||||
wrapThrows(() => localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(initialErrorConfig)))();
|
||||
throw new Error("Could not initialize formbricks");
|
||||
};
|
||||
|
||||
export const checkInitialized = (): Result<void, NotInitializedError> => {
|
||||
logger.debug("Check if initialized");
|
||||
if (!isInitialized || !ErrorHandler.initialized) {
|
||||
@@ -163,8 +201,19 @@ export const checkInitialized = (): Result<void, NotInitializedError> => {
|
||||
|
||||
export const deinitalize = (): void => {
|
||||
logger.debug("Deinitializing");
|
||||
closeSurvey();
|
||||
removeWidgetContainer();
|
||||
setIsSurveyRunning(false);
|
||||
removeAllEventListeners();
|
||||
config.resetConfig();
|
||||
isInitialized = false;
|
||||
setIsInitialized(false);
|
||||
};
|
||||
|
||||
export const putFormbricksInErrorState = (): void => {
|
||||
logger.debug("Putting formbricks in error state");
|
||||
// change formbricks status to error
|
||||
config.update({
|
||||
...config.get(),
|
||||
status: "error",
|
||||
expiresAt: new Date(new Date().getTime() + 10 * 60000), // 10 minutes in the future
|
||||
});
|
||||
deinitalize();
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "./errors";
|
||||
import { deinitalize, initialize } from "./initialize";
|
||||
import { Logger } from "./logger";
|
||||
import { sync } from "./sync";
|
||||
import { closeSurvey } from "./widget";
|
||||
|
||||
const config = Config.getInstance();
|
||||
@@ -55,15 +54,7 @@ export const updatePersonAttribute = async (
|
||||
}
|
||||
|
||||
if (res.data.changed) {
|
||||
logger.debug("Attribute updated. Syncing...");
|
||||
await sync(
|
||||
{
|
||||
environmentId: environmentId,
|
||||
apiHost: apiHost,
|
||||
userId: userId,
|
||||
},
|
||||
true
|
||||
);
|
||||
logger.debug("Attribute updated in Formbricks");
|
||||
}
|
||||
|
||||
return okVoid();
|
||||
@@ -178,6 +169,7 @@ export const setPersonAttribute = async (
|
||||
|
||||
export const logoutPerson = async (): Promise<void> => {
|
||||
deinitalize();
|
||||
config.resetConfig();
|
||||
};
|
||||
|
||||
export const resetPerson = async (): Promise<Result<void, NetworkError>> => {
|
||||
|
||||
+55
-48
@@ -15,21 +15,42 @@ const syncWithBackend = async (
|
||||
{ apiHost, environmentId, userId }: TJsSyncParams,
|
||||
noCache: boolean
|
||||
): Promise<Result<TJsStateSync, NetworkError>> => {
|
||||
const baseUrl = `${apiHost}/api/v1/client/${environmentId}/in-app/sync`;
|
||||
const urlSuffix = `?version=${import.meta.env.VERSION}`;
|
||||
try {
|
||||
const baseUrl = `${apiHost}/api/v1/client/${environmentId}/in-app/sync`;
|
||||
const urlSuffix = `?version=${import.meta.env.VERSION}`;
|
||||
|
||||
let fetchOptions: RequestInit = {};
|
||||
let fetchOptions: RequestInit = {};
|
||||
|
||||
if (noCache || getIsDebug()) {
|
||||
fetchOptions.cache = "no-cache";
|
||||
logger.debug("No cache option set for sync");
|
||||
}
|
||||
if (noCache || getIsDebug()) {
|
||||
fetchOptions.cache = "no-cache";
|
||||
logger.debug("No cache option set for sync");
|
||||
}
|
||||
|
||||
// if user id is available
|
||||
// if user id is not available
|
||||
if (!userId) {
|
||||
const url = baseUrl + urlSuffix;
|
||||
// public survey
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const jsonRes = await response.json();
|
||||
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: response.status,
|
||||
message: "Error syncing with backend",
|
||||
url,
|
||||
responseMessage: jsonRes.message,
|
||||
});
|
||||
}
|
||||
|
||||
return ok((await response.json()).data as TJsState);
|
||||
}
|
||||
|
||||
// userId is available, call the api with the `userId` param
|
||||
|
||||
const url = `${baseUrl}/${userId}${urlSuffix}`;
|
||||
|
||||
if (!userId) {
|
||||
const url = baseUrl + urlSuffix;
|
||||
// public survey
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -44,38 +65,20 @@ const syncWithBackend = async (
|
||||
});
|
||||
}
|
||||
|
||||
return ok((await response.json()).data as TJsState);
|
||||
const data = await response.json();
|
||||
const { data: state } = data;
|
||||
|
||||
return ok(state as TJsStateSync);
|
||||
} catch (e) {
|
||||
return err(e as NetworkError);
|
||||
}
|
||||
|
||||
// userId is available, call the api with the `userId` param
|
||||
|
||||
const url = `${baseUrl}/${userId}${urlSuffix}`;
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const jsonRes = await response.json();
|
||||
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: response.status,
|
||||
message: "Error syncing with backend",
|
||||
url,
|
||||
responseMessage: jsonRes.message,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const { data: state } = data;
|
||||
|
||||
return ok(state as TJsStateSync);
|
||||
};
|
||||
|
||||
export const sync = async (params: TJsSyncParams, noCache = false): Promise<void> => {
|
||||
try {
|
||||
const syncResult = await syncWithBackend(params, noCache);
|
||||
|
||||
if (syncResult?.ok !== true) {
|
||||
logger.error(`Sync failed: ${JSON.stringify(syncResult.error)}`);
|
||||
throw syncResult.error;
|
||||
}
|
||||
|
||||
@@ -115,8 +118,6 @@ export const sync = async (params: TJsSyncParams, noCache = false): Promise<void
|
||||
userId: params.userId,
|
||||
state,
|
||||
});
|
||||
|
||||
// before finding the surveys, check for public use
|
||||
} catch (error) {
|
||||
logger.error(`Error during sync: ${error}`);
|
||||
throw error;
|
||||
@@ -175,17 +176,23 @@ export const addExpiryCheckListener = (): void => {
|
||||
// add event listener to check sync with backend on regular interval
|
||||
if (typeof window !== "undefined" && syncIntervalId === null) {
|
||||
syncIntervalId = window.setInterval(async () => {
|
||||
// check if the config has not expired yet
|
||||
if (config.get().expiresAt && new Date(config.get().expiresAt) >= new Date()) {
|
||||
return;
|
||||
try {
|
||||
// check if the config has not expired yet
|
||||
if (config.get().expiresAt && new Date(config.get().expiresAt) >= new Date()) {
|
||||
return;
|
||||
}
|
||||
logger.debug("Config has expired. Starting sync.");
|
||||
await sync({
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
userId: config.get().userId,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Error during expiry check: ${e}`);
|
||||
logger.debug("Extending config and try again later.");
|
||||
const existingConfig = config.get();
|
||||
config.update(existingConfig);
|
||||
}
|
||||
logger.debug("Config has expired. Starting sync.");
|
||||
await sync({
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
userId: config.get().userId,
|
||||
// personId: config.get().state?.person?.id,
|
||||
});
|
||||
}, updateInterval);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,23 +7,29 @@ import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
import { Config } from "./config";
|
||||
import { ErrorHandler } from "./errors";
|
||||
import { putFormbricksInErrorState } from "./initialize";
|
||||
import { Logger } from "./logger";
|
||||
import { filterPublicSurveys, sync } from "./sync";
|
||||
|
||||
const containerId = "formbricks-web-container";
|
||||
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
const errorHandler = ErrorHandler.getInstance();
|
||||
let surveyRunning = false;
|
||||
let isSurveyRunning = false;
|
||||
let setIsError = (_: boolean) => {};
|
||||
let setIsResponseSendingFinished = (_: boolean) => {};
|
||||
|
||||
export const setIsSurveyRunning = (value: boolean) => {
|
||||
isSurveyRunning = value;
|
||||
};
|
||||
|
||||
export const renderWidget = async (survey: TSurvey) => {
|
||||
if (surveyRunning) {
|
||||
if (isSurveyRunning) {
|
||||
logger.debug("A survey is already running. Skipping.");
|
||||
return;
|
||||
}
|
||||
surveyRunning = true;
|
||||
setIsSurveyRunning(false);
|
||||
|
||||
if (survey.delay) {
|
||||
logger.debug(`Delaying survey by ${survey.delay} seconds.`);
|
||||
@@ -163,7 +169,7 @@ export const renderWidget = async (survey: TSurvey) => {
|
||||
|
||||
export const closeSurvey = async (): Promise<void> => {
|
||||
// remove container element from DOM
|
||||
document.getElementById(containerId)?.remove();
|
||||
removeWidgetContainer();
|
||||
addWidgetContainer();
|
||||
|
||||
// if unidentified user, refilter the surveys
|
||||
@@ -174,7 +180,7 @@ export const closeSurvey = async (): Promise<void> => {
|
||||
...config.get(),
|
||||
state: updatedState,
|
||||
});
|
||||
surveyRunning = false;
|
||||
setIsSurveyRunning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -188,9 +194,10 @@ export const closeSurvey = async (): Promise<void> => {
|
||||
},
|
||||
true
|
||||
);
|
||||
surveyRunning = false;
|
||||
} catch (e) {
|
||||
setIsSurveyRunning(false);
|
||||
} catch (e: any) {
|
||||
errorHandler.handle(e);
|
||||
putFormbricksInErrorState();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -200,6 +207,10 @@ export const addWidgetContainer = (): void => {
|
||||
document.body.appendChild(containerElement);
|
||||
};
|
||||
|
||||
export const removeWidgetContainer = (): void => {
|
||||
document.getElementById(containerId)?.remove();
|
||||
};
|
||||
|
||||
const loadFormbricksSurveysExternally = (): Promise<typeof window.formbricksSurveys> => {
|
||||
const formbricksSurveysScriptSrc = import.meta.env.FORMBRICKS_SURVEYS_SCRIPT_SRC;
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ export const ZJsConfig = z.object({
|
||||
userId: z.string().optional(),
|
||||
state: ZJsState,
|
||||
expiresAt: z.date(),
|
||||
status: z.enum(["success", "error"]).optional(),
|
||||
});
|
||||
|
||||
export type TJsConfig = z.infer<typeof ZJsConfig>;
|
||||
@@ -87,6 +88,8 @@ export const ZJsConfigUpdateInput = z.object({
|
||||
apiHost: z.string(),
|
||||
userId: z.string().optional(),
|
||||
state: ZJsState,
|
||||
expiresAt: z.date().optional(),
|
||||
status: z.enum(["success", "error"]).optional(),
|
||||
});
|
||||
|
||||
export type TJsConfigUpdateInput = z.infer<typeof ZJsConfigUpdateInput>;
|
||||
|
||||
Reference in New Issue
Block a user