fix: survey timeout during routing (#4515)

This commit is contained in:
Anshuman Pandey
2024-12-30 15:36:31 +05:30
committed by GitHub
parent 1c1ef56e00
commit 1fe625a9b4
4 changed files with 89 additions and 8 deletions

View File

@@ -108,9 +108,6 @@ const AppPage = ({}) => {
Look at the logs to understand how the widget works.{" "}
<strong className="dark:text-white">Open your browser console</strong> to see the logs.
</p>
{/* <div className="max-h-[40vh] overflow-y-auto py-4">
<LogsContainer />
</div> */}
</div>
</div>

View File

@@ -3,17 +3,21 @@ import { trackNoCodeAction } from "./actions";
import { Config } from "./config";
import { ErrorHandler, NetworkError, Result, err, match, okVoid } from "./errors";
import { Logger } from "./logger";
import { TimeoutStack } from "./timeout-stack";
import { evaluateNoCodeConfigClick, handleUrlFilters } from "./utils";
import { setIsSurveyRunning } from "./widget";
const appConfig = Config.getInstance();
const logger = Logger.getInstance();
const errorHandler = ErrorHandler.getInstance();
const timeoutStack = TimeoutStack.getInstance();
// Event types for various listeners
const events = ["hashchange", "popstate", "pushstate", "replacestate", "load"];
// Page URL Event Handlers
let arePageUrlEventListenersAdded = false;
let isHistoryPatched = false;
export const checkPageUrl = async (): Promise<Result<void, NetworkError>> => {
logger.debug(`Checking page url: ${window.location.href}`);
@@ -27,19 +31,48 @@ export const checkPageUrl = async (): Promise<Result<void, NetworkError>> => {
const urlFilters = event.noCodeConfig?.urlFilters ?? [];
const isValidUrl = handleUrlFilters(urlFilters);
if (!isValidUrl) continue;
if (isValidUrl) {
const trackResult = await trackNoCodeAction(event.name);
const trackResult = await trackNoCodeAction(event.name);
if (trackResult.ok !== true) return err(trackResult.error);
if (trackResult.ok !== true) {
return err(trackResult.error);
}
} else {
const scheduledTimeouts = timeoutStack.getTimeouts();
const scheduledTimeout = scheduledTimeouts.find((timeout) => timeout.event === event.name);
// If invalid, clear if it's scheduled
if (scheduledTimeout) {
timeoutStack.remove(scheduledTimeout.timeoutId);
setIsSurveyRunning(false);
}
}
}
return okVoid();
};
const checkPageUrlWrapper = () => checkPageUrl();
const checkPageUrlWrapper = () => {
checkPageUrl();
};
export const addPageUrlEventListeners = (): void => {
if (typeof window === "undefined" || arePageUrlEventListenersAdded) return;
// Monkey patch history methods if not already done
if (!isHistoryPatched) {
const originalPushState = history.pushState;
history.pushState = function (...args) {
const returnValue = originalPushState.apply(this, args);
const event = new Event("pushstate");
window.dispatchEvent(event);
return returnValue;
};
isHistoryPatched = true;
}
events.forEach((event) => window.addEventListener(event, checkPageUrlWrapper));
arePageUrlEventListenersAdded = true;
};

View File

@@ -0,0 +1,43 @@
export class TimeoutStack {
private static instance: TimeoutStack;
// private timeouts: number[] = [];
private timeouts: { event: string; timeoutId: number }[] = [];
// Private constructor to prevent direct instantiation
private constructor() {}
// Retrieve the singleton instance of TimeoutStack
public static getInstance(): TimeoutStack {
if (!TimeoutStack.instance) {
TimeoutStack.instance = new TimeoutStack();
}
return TimeoutStack.instance;
}
// Add a new timeout ID to the stack
public add(event: string, timeoutId: number): void {
this.timeouts.push({ event, timeoutId });
}
// Clear a specific timeout and remove it from the stack
public remove(timeoutId: number): void {
clearTimeout(timeoutId);
this.timeouts = this.timeouts.filter((timeout) => timeout.timeoutId !== timeoutId);
}
// Clear all timeouts and reset the stack
public clear(): void {
for (const timeout of this.timeouts) {
clearTimeout(timeout.timeoutId);
}
this.timeouts = [];
}
// Get the current stack of timeout IDs
public getTimeouts(): {
event: string;
timeoutId: number;
}[] {
return this.timeouts;
}
}

View File

@@ -13,6 +13,7 @@ import { TUploadFileConfig } from "@formbricks/types/storage";
import { Config } from "./config";
import { CONTAINER_ID } from "./constants";
import { Logger } from "./logger";
import { TimeoutStack } from "./timeout-stack";
import {
filterSurveys,
getDefaultLanguageCode,
@@ -23,6 +24,8 @@ import {
const config = Config.getInstance();
const logger = Logger.getInstance();
const timeoutStack = TimeoutStack.getInstance();
let isSurveyRunning = false;
let setIsError = (_: boolean) => {};
let setIsResponseSendingFinished = (_: boolean) => {};
@@ -62,6 +65,7 @@ const renderWidget = async (
logger.debug("A survey is already running. Skipping.");
return;
}
setIsSurveyRunning(true);
if (survey.delay) {
@@ -108,7 +112,7 @@ const renderWidget = async (
const isBrandingEnabled = project.inAppSurveyBranding;
const formbricksSurveys = await loadFormbricksSurveysExternally();
setTimeout(() => {
const timeoutId = setTimeout(() => {
formbricksSurveys.renderSurveyModal({
survey,
isBrandingEnabled,
@@ -236,6 +240,10 @@ const renderWidget = async (
hiddenFieldsRecord: hiddenFields,
});
}, survey.delay * 1000);
if (action) {
timeoutStack.add(action, timeoutId as unknown as number);
}
};
export const closeSurvey = async (): Promise<void> => {