mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-11 11:09:01 -05:00
feat: add trigger after time passed (#7452)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -47,6 +47,7 @@ const createNoCodeConfigType = (t: ReturnType<typeof useTranslation>["t"]) => ({
|
||||
pageView: t("environments.actions.page_view"),
|
||||
exitIntent: t("environments.actions.exit_intent"),
|
||||
fiftyPercentScroll: t("environments.actions.fifty_percent_scroll"),
|
||||
pageDwell: t("environments.actions.time_on_page"),
|
||||
});
|
||||
|
||||
const formatRecontactDaysString = (days: number, t: ReturnType<typeof useTranslation>["t"]) => {
|
||||
|
||||
@@ -569,10 +569,15 @@ checksums:
|
||||
environments/actions/test_match: 401f40fa811c3945bda3e019f242e388
|
||||
environments/actions/test_your_url: 4c35b97b614d4f20f92aa449cc748e8d
|
||||
environments/actions/this_action_was_created_automatically_you_cannot_make_changes_to_it: 6eddf2420a18408b9c8d9db6ab5ca34b
|
||||
environments/actions/this_action_will_be_triggered_after_user_stays_on_page: 3ae729d24e828fdd473a233078f91074
|
||||
environments/actions/this_action_will_be_triggered_when_the_page_is_loaded: 8d28f30cf56f50ea79aef8dd2b02185d
|
||||
environments/actions/this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page: 60230ad129853377af1066e10f3b3c22
|
||||
environments/actions/this_action_will_be_triggered_when_the_user_tries_to_leave_the_page: 504fa734659524933e3489823d820265
|
||||
environments/actions/this_is_a_code_action_please_make_changes_in_your_code_base: 27e64863fdebca791ca129ac0c3da3d5
|
||||
environments/actions/time_in_seconds: 822be76950e5f614ed23f52ba1d4825f
|
||||
environments/actions/time_in_seconds_placeholder: e54a4c40e0c6b43fb2e97bd32cab8da8
|
||||
environments/actions/time_in_seconds_with_unit: a743b7844c71ddad364a93872682ae9e
|
||||
environments/actions/time_on_page: b712a3e809669be7384a52a801777408
|
||||
environments/actions/track_new_user_action: 7133de99e7561128cfc6631ab2e592de
|
||||
environments/actions/track_user_action_to_display_surveys_or_create_user_segment: 2794a2dc51e48f1178d5d759db68029c
|
||||
environments/actions/url: ca97457614226960d41dd18c3c29c86b
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Testspiel",
|
||||
"test_your_url": "Teste deine URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Diese Aktion wurde automatisch erstellt. Du kannst keine Änderungen daran vornehmen.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Diese Aktion wird ausgelöst, nachdem der Nutzer für die angegebene Dauer auf der Seite geblieben ist.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Diese Aktion wird ausgelöst, wenn die Seite geladen ist.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Diese Aktion wird ausgelöst, wenn der Benutzer 50% der Seite scrollt.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Diese Aktion wird ausgelöst, wenn der Benutzer versucht, die Seite zu verlassen.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Dies ist eine Code-Aktion. Bitte nehmen Sie Änderungen an Ihrem Code vor.",
|
||||
"time_in_seconds": "Zeit in Sekunden",
|
||||
"time_in_seconds_placeholder": "z.B. 10",
|
||||
"time_in_seconds_with_unit": "{seconds} s",
|
||||
"time_on_page": "Verweildauer auf Seite",
|
||||
"track_new_user_action": "Neue Benutzeraktion verfolgen",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Benutzeraktionen verfolgen, um Umfragen anzuzeigen oder Benutzersegmente zu erstellen.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Test Match",
|
||||
"test_your_url": "Test your URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "This action was created automatically. You cannot make changes to it.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "This action will be triggered after the user stays on the page for the specified duration.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "This action will be triggered when the page is loaded.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "This action will be triggered when the user scrolls 50% of the page.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "This action will be triggered when the user tries to leave the page.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "This is a code action. Please make changes in your code base.",
|
||||
"time_in_seconds": "Time in seconds",
|
||||
"time_in_seconds_placeholder": "e.g. 10",
|
||||
"time_in_seconds_with_unit": "{seconds}s",
|
||||
"time_on_page": "Time on Page",
|
||||
"track_new_user_action": "Track New User Action",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Track user action to display surveys or create user segment.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Probar coincidencia",
|
||||
"test_your_url": "Prueba tu URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Esta acción se creó automáticamente. No puedes realizar cambios en ella.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Esta acción se activará después de que el usuario permanezca en la página durante el tiempo especificado.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Esta acción se activará cuando se cargue la página.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Esta acción se activará cuando el usuario desplace el 50 % de la página.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Esta acción se activará cuando el usuario intente abandonar la página.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Esta es una acción de código. Por favor, realiza cambios en tu base de código.",
|
||||
"time_in_seconds": "Tiempo en segundos",
|
||||
"time_in_seconds_placeholder": "p. ej. 10",
|
||||
"time_in_seconds_with_unit": "{seconds}s",
|
||||
"time_on_page": "Tiempo en la página",
|
||||
"track_new_user_action": "Seguir nueva acción de usuario",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Seguir acción de usuario para mostrar encuestas o crear segmento de usuarios.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Match de test",
|
||||
"test_your_url": "Test de l'URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Cette action a été créée automatiquement. Vous ne pouvez pas y apporter de modifications.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Cette action sera déclenchée après que l'utilisateur reste sur la page pendant la durée spécifiée.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Cette action se déclenche quand une page est chargée.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Cette action se déclenche quand un utilisateur consulte 50 % d'une page.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Cette action se déclenche quand un utilisateur tente de quitter une page.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Ceci est une action de code. Veuillez apporter des modifications à votre base de code.",
|
||||
"time_in_seconds": "Temps en secondes",
|
||||
"time_in_seconds_placeholder": "par ex. 10",
|
||||
"time_in_seconds_with_unit": "{seconds} s",
|
||||
"time_on_page": "Temps sur la page",
|
||||
"track_new_user_action": "Suivi des actions d'un utilisateur",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Vous pouvez suivre les actions d'un utilisateur pour afficher des enquêtes ou créer des segments ad hoc.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Illeszkedés tesztelése",
|
||||
"test_your_url": "URL tesztelése",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Ez a művelet automatikusan lett létrehozva. Nem végezhet változtatásokat rajta.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Ez a művelet akkor fog aktiválódni, miután a felhasználó a megadott ideig az oldalon tartózkodik.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Ez a művelet akkor lesz aktiválva, ha az oldal betöltődik.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Ez a művelet akkor lesz aktiválva, ha a felhasználó az oldal 50%-áig görget.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Ez a művelet akkor lesz aktiválva, ha a felhasználó megpróbálja elhagyni az oldalt.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Ez egy kódművelet. A változtatásokat a kódbázisban hajtsa végre.",
|
||||
"time_in_seconds": "Idő másodpercben",
|
||||
"time_in_seconds_placeholder": "pl. 10",
|
||||
"time_in_seconds_with_unit": "{seconds} mp",
|
||||
"time_on_page": "Oldalon töltött idő",
|
||||
"track_new_user_action": "Új felhasználói művelet követése",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Felhasználói művelet követése a kérdőívek megjelenítéséhez vagy felhasználói szakasz létrehozásához.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "一致をテスト",
|
||||
"test_your_url": "URLをテスト",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "このアクションは自動的に作成されました。変更を加えることはできません。",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "このアクションは、ユーザーが指定された時間ページに滞在した後にトリガーされます。",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "このアクションは、ページが読み込まれたときにトリガーされます。",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "このアクションは、ユーザーがページの50%をスクロールしたときにトリガーされます。",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "このアクションは、ユーザーがページを離れようとしたときにトリガーされます。",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "これはコードアクションです。コードベースで変更を行ってください。",
|
||||
"time_in_seconds": "秒数",
|
||||
"time_in_seconds_placeholder": "例: 10",
|
||||
"time_in_seconds_with_unit": "{seconds}秒",
|
||||
"time_on_page": "ページ滞在時間",
|
||||
"track_new_user_action": "新しいユーザーアクションを追跡",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "ユーザーアクションを追跡してフォームを表示したり、ユーザーセグメントを作成したりします。",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Testwedstrijd",
|
||||
"test_your_url": "Test uw URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Deze actie is automatisch aangemaakt. U kunt er geen wijzigingen in aanbrengen.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Deze actie wordt geactiveerd nadat de gebruiker gedurende de opgegeven tijd op de pagina blijft.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Deze actie wordt geactiveerd wanneer de pagina wordt geladen.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Deze actie wordt geactiveerd wanneer de gebruiker 50% van de pagina scrollt.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Deze actie wordt geactiveerd wanneer de gebruiker de pagina probeert te verlaten.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Dit is een codeactie. Breng wijzigingen aan in uw codebasis.",
|
||||
"time_in_seconds": "Tijd in seconden",
|
||||
"time_in_seconds_placeholder": "bijv. 10",
|
||||
"time_in_seconds_with_unit": "{seconds}s",
|
||||
"time_on_page": "Tijd op pagina",
|
||||
"track_new_user_action": "Volg nieuwe gebruikersactie",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Volg gebruikersacties om enquêtes weer te geven of een gebruikerssegment te creëren.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Partida de Teste",
|
||||
"test_your_url": "Teste sua URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Essa ação foi criada automaticamente. Você não pode fazer alterações nela.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Esta ação será acionada depois que o usuário permanecer na página pelo tempo especificado.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Essa ação vai ser disparada quando a página carregar.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Essa ação vai ser acionada quando o usuário rolar 50% da página.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Essa ação será acionada quando o usuário tentar sair da página.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Esta é uma ação de código. Por favor, faça alterações na sua base de código.",
|
||||
"time_in_seconds": "Tempo em segundos",
|
||||
"time_in_seconds_placeholder": "ex: 10",
|
||||
"time_in_seconds_with_unit": "{seconds}s",
|
||||
"time_on_page": "Tempo na Página",
|
||||
"track_new_user_action": "Rastrear Ação de Novo Usuário",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Rastrear ações do usuário para exibir pesquisas ou criar segmento de usuários.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Testar correspondência",
|
||||
"test_your_url": "Testar o seu URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Esta ação foi criada automaticamente. Não pode fazer alterações a esta ação.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Esta ação será acionada depois de o utilizador permanecer na página durante o tempo especificado.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Esta ação será desencadeada quando a página for carregada.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Esta ação será desencadeada quando o utilizador rolar 50% da página.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Esta ação será desencadeada quando o utilizador tentar sair da página.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Esta é uma ação de código. Por favor, faça alterações na sua base de código.",
|
||||
"time_in_seconds": "Tempo em segundos",
|
||||
"time_in_seconds_placeholder": "ex. 10",
|
||||
"time_in_seconds_with_unit": "{seconds}s",
|
||||
"time_on_page": "Tempo na Página",
|
||||
"track_new_user_action": "Rastrear Nova Ação do Utilizador",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Rastrear ação do utilizador para exibir inquéritos ou criar segmento de utilizador.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Testează potrivirea",
|
||||
"test_your_url": "Testează URL-ul tău",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Această acțiune a fost creată automat. Nu puteți face modificări la aceasta.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Această acțiune va fi declanșată după ce utilizatorul rămâne pe pagină pentru durata specificată.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Această acțiune va fi declanșată atunci când pagina este încărcată.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Această acțiune va fi declanșată atunci când utilizatorul derulează 50% din pagină.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Această acțiune va fi declanșată atunci când utilizatorul încearcă să părăsească pagina.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Aceasta este o acțiune de cod. Vă rugăm să faceți modificări în baza dvs. de cod.",
|
||||
"time_in_seconds": "Timp în secunde",
|
||||
"time_in_seconds_placeholder": "ex. 10",
|
||||
"time_in_seconds_with_unit": "{seconds}s",
|
||||
"time_on_page": "Timp pe pagină",
|
||||
"track_new_user_action": "Urmăriți acțiunea noului utilizator",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Urmăriți acțiunea utilizatorului pentru a afișa sondaje sau a crea un segment de utilizator.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Проверить соответствие",
|
||||
"test_your_url": "Проверьте свой URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Это действие создано автоматически. Вы не можете вносить в него изменения.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Это действие будет запущено после того, как пользователь пробудет на странице в течение указанного времени.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Это действие будет выполнено при загрузке страницы.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Это действие будет выполнено, когда пользователь прокрутит 50% страницы.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Это действие будет выполнено, когда пользователь попытается покинуть страницу.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Это действие с кодом. Пожалуйста, внесите изменения в ваш код.",
|
||||
"time_in_seconds": "Время в секундах",
|
||||
"time_in_seconds_placeholder": "например, 10",
|
||||
"time_in_seconds_with_unit": "{seconds} с",
|
||||
"time_on_page": "Время на странице",
|
||||
"track_new_user_action": "Отслеживать новое действие пользователя",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Отслеживайте действия пользователя для показа опросов или создания сегмента пользователей.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "Testa matchning",
|
||||
"test_your_url": "Testa din URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "Denna åtgärd skapades automatiskt. Du kan inte göra ändringar i den.",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "Denna åtgärd utlöses när användaren har stannat kvar på sidan under den angivna tiden.",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "Denna åtgärd utlöses när sidan laddas.",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "Denna åtgärd utlöses när användaren scrollar 50% av sidan.",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "Denna åtgärd utlöses när användaren försöker lämna sidan.",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "Detta är en kodåtgärd. Vänligen gör ändringar i din kodbas.",
|
||||
"time_in_seconds": "Tid i sekunder",
|
||||
"time_in_seconds_placeholder": "t.ex. 10",
|
||||
"time_in_seconds_with_unit": "{seconds}s",
|
||||
"time_on_page": "Tid på sidan",
|
||||
"track_new_user_action": "Spåra ny användaråtgärd",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "Spåra användaråtgärder för att visa enkäter eller skapa användarsegment.",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "测试匹配",
|
||||
"test_your_url": "测试你的URL",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "此操作是自动创建的。您 不能对此进行更改。",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "此操作将在用户停留在页面达到指定时长后触发。",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "页面 加载 时,将 触发 此 操作",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "当 用户 滚动 页面 50% 时,将 触发 此 操作",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "当 用户 试图 离开 页面 时,将 触发 此 操作",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "这是一个代码操作。请在您的代码库中进行更改。",
|
||||
"time_in_seconds": "时长(秒)",
|
||||
"time_in_seconds_placeholder": "例如 10",
|
||||
"time_in_seconds_with_unit": "{seconds}秒",
|
||||
"time_on_page": "页面停留时间",
|
||||
"track_new_user_action": "跟踪 新用户 操作",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "跟踪 用户 操作以显示调查 或 创建用户段",
|
||||
"url": "URL",
|
||||
|
||||
@@ -601,10 +601,15 @@
|
||||
"test_match": "測試比對",
|
||||
"test_your_url": "測試您的網址",
|
||||
"this_action_was_created_automatically_you_cannot_make_changes_to_it": "此操作是自動建立的。您無法對其進行變更。",
|
||||
"this_action_will_be_triggered_after_user_stays_on_page": "此動作將在使用者停留於頁面達到指定時間後觸發。",
|
||||
"this_action_will_be_triggered_when_the_page_is_loaded": "當頁面載入時,將觸發此操作。",
|
||||
"this_action_will_be_triggered_when_the_user_scrolls_50_percent_of_the_page": "當使用者捲動頁面 50% 時,將觸發此操作。",
|
||||
"this_action_will_be_triggered_when_the_user_tries_to_leave_the_page": "當使用者嘗試離開頁面時,將觸發此操作。",
|
||||
"this_is_a_code_action_please_make_changes_in_your_code_base": "這是一個 code 動作。請在您的 code base 中進行更改。",
|
||||
"time_in_seconds": "時間(秒)",
|
||||
"time_in_seconds_placeholder": "例如:10",
|
||||
"time_in_seconds_with_unit": "{seconds} 秒",
|
||||
"time_on_page": "頁面停留時間",
|
||||
"track_new_user_action": "追蹤新使用者操作",
|
||||
"track_user_action_to_display_surveys_or_create_user_segment": "追蹤使用者操作以顯示問卷或建立使用者區隔。",
|
||||
"url": "網址",
|
||||
|
||||
@@ -33,11 +33,16 @@ export const ActionClassInfo = ({ actionClass, className = "" }: ActionClassInfo
|
||||
};
|
||||
|
||||
const isNoCodeClick = actionClass.type === "noCode" && actionClass.noCodeConfig?.type === "click";
|
||||
const isNoCodeTimeOnPage = actionClass.type === "noCode" && actionClass.noCodeConfig?.type === "pageDwell";
|
||||
|
||||
const clickConfig = isNoCodeClick
|
||||
? (actionClass.noCodeConfig as Extract<typeof actionClass.noCodeConfig, { type: "click" }>)
|
||||
: null;
|
||||
|
||||
const timeOnPageConfig = isNoCodeTimeOnPage
|
||||
? (actionClass.noCodeConfig as Extract<typeof actionClass.noCodeConfig, { type: "pageDwell" }>)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className={`mt-1 text-xs text-slate-500 ${className}`}>
|
||||
{actionClass.description && <span className="mr-1">{actionClass.description}</span>}
|
||||
@@ -60,6 +65,17 @@ export const ActionClassInfo = ({ actionClass, className = "" }: ActionClassInfo
|
||||
</InfoItem>
|
||||
)}
|
||||
|
||||
{timeOnPageConfig && (
|
||||
<InfoItem>
|
||||
{t("environments.actions.time_in_seconds")}:{" "}
|
||||
<b>
|
||||
{t("environments.actions.time_in_seconds_with_unit", {
|
||||
seconds: timeOnPageConfig.timeInSeconds,
|
||||
})}
|
||||
</b>
|
||||
</InfoItem>
|
||||
)}
|
||||
|
||||
{renderUrlFilters()}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { TabToggle } from "@/modules/ui/components/tab-toggle";
|
||||
import { CssSelector } from "./components/css-selector";
|
||||
@@ -40,6 +41,7 @@ export const NoCodeActionForm = ({ form, isReadOnly }: NoCodeActionFormProps) =>
|
||||
{ value: "pageView", label: t("environments.actions.page_view") },
|
||||
{ value: "exitIntent", label: t("environments.actions.exit_intent") },
|
||||
{ value: "fiftyPercentScroll", label: t("environments.actions.fifty_percent_scroll") },
|
||||
{ value: "pageDwell", label: t("environments.actions.time_on_page") },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -95,6 +97,42 @@ export const NoCodeActionForm = ({ form, isReadOnly }: NoCodeActionFormProps) =>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{watch("noCodeConfig.type") === "pageDwell" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Alert>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle>{t("environments.actions.time_on_page")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.actions.this_action_will_be_triggered_after_user_stays_on_page")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<FormField
|
||||
control={control}
|
||||
name="noCodeConfig.timeInSeconds"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>{t("environments.actions.time_in_seconds")}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={3600}
|
||||
placeholder={t("environments.actions.time_in_seconds_placeholder")}
|
||||
disabled={isReadOnly}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
field.onChange(val === "" ? undefined : Number(val));
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormError />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<PageUrlSelector form={form} isReadOnly={isReadOnly} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
addExitIntentListener,
|
||||
addPageUrlEventListeners,
|
||||
addScrollDepthListener,
|
||||
clearTimeOnPageTimers,
|
||||
removeClickEventListener,
|
||||
removeExitIntentListener,
|
||||
removePageUrlEventListeners,
|
||||
@@ -34,6 +35,7 @@ export const addCleanupEventListeners = (): void => {
|
||||
removeClickEventListener();
|
||||
removeExitIntentListener();
|
||||
removeScrollDepthListener();
|
||||
clearTimeOnPageTimers();
|
||||
});
|
||||
areRemoveEventListenersAdded = true;
|
||||
};
|
||||
@@ -47,6 +49,7 @@ export const removeCleanupEventListeners = (): void => {
|
||||
removeClickEventListener();
|
||||
removeExitIntentListener();
|
||||
removeScrollDepthListener();
|
||||
clearTimeOnPageTimers();
|
||||
});
|
||||
areRemoveEventListenersAdded = false;
|
||||
};
|
||||
@@ -58,5 +61,6 @@ export const removeAllEventListeners = (): void => {
|
||||
removeClickEventListener();
|
||||
removeExitIntentListener();
|
||||
removeScrollDepthListener();
|
||||
clearTimeOnPageTimers();
|
||||
removeCleanupEventListeners();
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ vi.mock("@/lib/survey/no-code-action", () => ({
|
||||
removeExitIntentListener: vi.fn(),
|
||||
addScrollDepthListener: vi.fn(),
|
||||
removeScrollDepthListener: vi.fn(),
|
||||
clearTimeOnPageTimers: vi.fn(),
|
||||
}));
|
||||
|
||||
// We'll need to track if "areRemoveEventListenersAdded" was set
|
||||
@@ -121,6 +122,7 @@ describe("event-listeners file", () => {
|
||||
const mockClickRemove = vi.spyOn(pageUrlEventListeners, "removeClickEventListener");
|
||||
const mockExitRemove = vi.spyOn(pageUrlEventListeners, "removeExitIntentListener");
|
||||
const mockScrollRemove = vi.spyOn(pageUrlEventListeners, "removeScrollDepthListener");
|
||||
const mockTimeOnPageClear = vi.spyOn(pageUrlEventListeners, "clearTimeOnPageTimers");
|
||||
|
||||
// Call the function after setting up the spies
|
||||
removeAllEventListeners();
|
||||
@@ -132,6 +134,7 @@ describe("event-listeners file", () => {
|
||||
expect(mockClickRemove).toHaveBeenCalled();
|
||||
expect(mockExitRemove).toHaveBeenCalled();
|
||||
expect(mockScrollRemove).toHaveBeenCalled();
|
||||
expect(mockTimeOnPageClear).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("removeAllEventListeners also calls removeCleanupEventListeners", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TimeoutStack } from "@/lib/common/timeout-stack";
|
||||
import { evaluateNoCodeConfigClick, handleUrlFilters } from "@/lib/common/utils";
|
||||
import { trackNoCodeAction } from "@/lib/survey/action";
|
||||
import { setIsSurveyRunning } from "@/lib/survey/widget";
|
||||
import { type TEnvironmentStateActionClass } from "@/types/config";
|
||||
import { type Result } from "@/types/error";
|
||||
|
||||
// Factory for creating context-specific tracking handlers
|
||||
@@ -28,6 +29,22 @@ const trackNoCodePageViewActionHandler = createTrackNoCodeActionWithContext("pag
|
||||
const trackNoCodeClickActionHandler = createTrackNoCodeActionWithContext("click");
|
||||
const trackNoCodeExitIntentActionHandler = createTrackNoCodeActionWithContext("exit intent");
|
||||
const trackNoCodeScrollActionHandler = createTrackNoCodeActionWithContext("scroll");
|
||||
const trackNoCodeTimeOnPageActionHandler = createTrackNoCodeActionWithContext("time on page");
|
||||
|
||||
// Time on Page timer state per action name
|
||||
interface TimeOnPageRunning {
|
||||
status: "running";
|
||||
pageKey: string;
|
||||
timerId: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
interface TimeOnPageFired {
|
||||
status: "fired";
|
||||
pageKey: string;
|
||||
}
|
||||
|
||||
type TimeOnPageState = TimeOnPageRunning | TimeOnPageFired;
|
||||
const timeOnPageTimers = new Map<string, TimeOnPageState>();
|
||||
|
||||
// Event types for various listeners
|
||||
const events = ["hashchange", "popstate", "pushstate", "replacestate", "load"];
|
||||
@@ -39,6 +56,69 @@ export const setIsHistoryPatched = (value: boolean): void => {
|
||||
isHistoryPatched = value;
|
||||
};
|
||||
|
||||
const checkTimeOnPage = (actionClasses: TEnvironmentStateActionClass[]): void => {
|
||||
const queue = CommandQueue.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
const timeoutStack = TimeoutStack.getInstance();
|
||||
|
||||
const noCodeTimeOnPageActionClasses = actionClasses.filter(
|
||||
(action) => action.type === "noCode" && action.noCodeConfig?.type === "pageDwell"
|
||||
);
|
||||
|
||||
const currentPageKey = window.location.href;
|
||||
const matchingTimeOnPageActionNames = new Set<string>();
|
||||
|
||||
for (const event of noCodeTimeOnPageActionClasses) {
|
||||
const config = event.noCodeConfig as Extract<typeof event.noCodeConfig, { type: "pageDwell" }>;
|
||||
|
||||
const { urlFilters, urlFiltersConnector: connector } = config;
|
||||
const isValidUrl = handleUrlFilters(urlFilters, connector ?? "or");
|
||||
|
||||
if (!isValidUrl) continue;
|
||||
|
||||
matchingTimeOnPageActionNames.add(event.name);
|
||||
|
||||
const existing = timeOnPageTimers.get(event.name);
|
||||
if (existing?.pageKey === currentPageKey) continue;
|
||||
|
||||
if (existing?.status === "running") {
|
||||
logger.debug(`Time on page timer for "${event.name}" restarting — page changed to ${currentPageKey}`);
|
||||
clearTimeout(existing.timerId);
|
||||
}
|
||||
|
||||
const { timeInSeconds } = config;
|
||||
const actionName = event.name;
|
||||
|
||||
logger.debug(`Starting time on page timer for "${actionName}" (${timeInSeconds.toString()}s)`);
|
||||
const timerId = globalThis.setTimeout(() => {
|
||||
logger.debug(
|
||||
`Time on page timer for "${actionName}" completed after ${timeInSeconds.toString()}s — firing action`
|
||||
);
|
||||
timeOnPageTimers.set(actionName, { status: "fired", pageKey: currentPageKey });
|
||||
void queue.add(trackNoCodeTimeOnPageActionHandler, CommandType.GeneralAction, true, actionName);
|
||||
}, timeInSeconds * 1000);
|
||||
timeOnPageTimers.set(actionName, { status: "running", pageKey: currentPageKey, timerId });
|
||||
}
|
||||
|
||||
for (const [actionName, entry] of timeOnPageTimers) {
|
||||
if (matchingTimeOnPageActionNames.has(actionName)) continue;
|
||||
|
||||
if (entry.status === "running") {
|
||||
logger.debug(
|
||||
`Time on page timer for "${actionName}" interrupted — user navigated away before completion`
|
||||
);
|
||||
clearTimeout(entry.timerId);
|
||||
}
|
||||
timeOnPageTimers.delete(actionName);
|
||||
|
||||
const scheduledTimeout = timeoutStack.getTimeouts().find((t) => t.event === actionName);
|
||||
if (!scheduledTimeout) continue;
|
||||
|
||||
timeoutStack.remove(scheduledTimeout.timeoutId);
|
||||
setIsSurveyRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkPageUrl = async (): Promise<Result<void, unknown>> => {
|
||||
const queue = CommandQueue.getInstance();
|
||||
const appConfig = Config.getInstance();
|
||||
@@ -71,6 +151,8 @@ export const checkPageUrl = async (): Promise<Result<void, unknown>> => {
|
||||
}
|
||||
}
|
||||
|
||||
checkTimeOnPage(actionClasses);
|
||||
|
||||
return { ok: true, data: undefined };
|
||||
};
|
||||
|
||||
@@ -258,3 +340,13 @@ export const removeScrollDepthListener = (): void => {
|
||||
scrollDepthListenerAdded = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Time on Page Cleanup
|
||||
export const clearTimeOnPageTimers = (): void => {
|
||||
for (const [, entry] of timeOnPageTimers) {
|
||||
if (entry.status === "running") {
|
||||
clearTimeout(entry.timerId);
|
||||
}
|
||||
}
|
||||
timeOnPageTimers.clear();
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
addPageUrlEventListeners,
|
||||
addScrollDepthListener,
|
||||
checkPageUrl,
|
||||
clearTimeOnPageTimers,
|
||||
createTrackNoCodeActionWithContext,
|
||||
removeClickEventListener,
|
||||
removeExitIntentListener,
|
||||
@@ -654,3 +655,280 @@ describe("removePageUrlEventListeners additional cases", () => {
|
||||
(window.removeEventListener as Mock).mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("time on page action handling", () => {
|
||||
let getInstanceConfigMock: MockInstance<() => Config>;
|
||||
let getInstanceTimeoutStackMock: MockInstance<() => TimeoutStack>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
clearTimeOnPageTimers();
|
||||
getInstanceConfigMock = vi.spyOn(Config, "getInstance");
|
||||
getInstanceTimeoutStackMock = vi.spyOn(TimeoutStack, "getInstance");
|
||||
(checkSetup as Mock).mockReturnValue({ ok: true });
|
||||
|
||||
const mockTimeoutStack = {
|
||||
getTimeouts: vi.fn().mockReturnValue([]),
|
||||
remove: vi.fn(),
|
||||
add: vi.fn(),
|
||||
};
|
||||
getInstanceTimeoutStackMock.mockReturnValue(mockTimeoutStack as unknown as TimeoutStack);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.resetAllMocks();
|
||||
clearTimeOnPageTimers();
|
||||
});
|
||||
|
||||
const createConfigWithTimeOnPageAction = (actionName: string, timeInSeconds: number) => ({
|
||||
get: vi.fn().mockReturnValue({
|
||||
environment: {
|
||||
data: {
|
||||
actionClasses: [
|
||||
{
|
||||
name: actionName,
|
||||
type: "noCode",
|
||||
noCodeConfig: {
|
||||
type: "pageDwell",
|
||||
urlFilters: [{ value: "/dashboard", rule: "contains" }],
|
||||
timeInSeconds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
update: vi.fn(),
|
||||
});
|
||||
|
||||
test("starts a timer when URL matches a time on page action", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
(trackNoCodeAction as Mock).mockResolvedValue({ ok: true });
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 5);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
|
||||
expect(trackNoCodeAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fires trackNoCodeAction after the time on page elapses", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
(trackNoCodeAction as Mock).mockResolvedValue({ ok: true });
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 5);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
|
||||
expect(trackNoCodeAction).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
expect(trackNoCodeAction).toHaveBeenCalledWith("dwellAction");
|
||||
});
|
||||
|
||||
test("does not start a timer if URL does not match", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(false);
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 5);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/settings" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(trackNoCodeAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("clears time on page timer when URL stops matching on subsequent navigation", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
(trackNoCodeAction as Mock).mockResolvedValue({ ok: true });
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 10);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
expect(trackNoCodeAction).not.toHaveBeenCalled();
|
||||
|
||||
(handleUrlFilters as Mock).mockReturnValue(false);
|
||||
await checkPageUrl();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10000);
|
||||
expect(trackNoCodeAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not restart timer when URL still matches on subsequent checkPageUrl calls", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
(trackNoCodeAction as Mock).mockResolvedValue({ ok: true });
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 5);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
|
||||
await checkPageUrl();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
expect(trackNoCodeAction).toHaveBeenCalledTimes(1);
|
||||
expect(trackNoCodeAction).toHaveBeenCalledWith("dwellAction");
|
||||
});
|
||||
|
||||
test("restarts time on page timer when page URL changes but still matches the filter", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
(trackNoCodeAction as Mock).mockResolvedValue({ ok: true });
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 5);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard/1" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
expect(trackNoCodeAction).not.toHaveBeenCalled();
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard/2" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
expect(trackNoCodeAction).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
expect(trackNoCodeAction).toHaveBeenCalledTimes(1);
|
||||
expect(trackNoCodeAction).toHaveBeenCalledWith("dwellAction");
|
||||
});
|
||||
|
||||
test("restarts timer after navigating away and back", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
(trackNoCodeAction as Mock).mockResolvedValue({ ok: true });
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 5);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
|
||||
(handleUrlFilters as Mock).mockReturnValue(false);
|
||||
await checkPageUrl();
|
||||
|
||||
expect(trackNoCodeAction).not.toHaveBeenCalled();
|
||||
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
await checkPageUrl();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(trackNoCodeAction).toHaveBeenCalledTimes(1);
|
||||
expect(trackNoCodeAction).toHaveBeenCalledWith("dwellAction");
|
||||
});
|
||||
|
||||
test("does not re-fire after timer already fired on same page", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
(trackNoCodeAction as Mock).mockResolvedValue({ ok: true });
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 3);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
expect(trackNoCodeAction).toHaveBeenCalledTimes(1);
|
||||
|
||||
await checkPageUrl();
|
||||
await vi.advanceTimersByTimeAsync(3000);
|
||||
expect(trackNoCodeAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("clearTimeOnPageTimers clears all active timers", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
(trackNoCodeAction as Mock).mockResolvedValue({ ok: true });
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 5);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
|
||||
clearTimeOnPageTimers();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(trackNoCodeAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("clears scheduled survey display timeout when URL stops matching a time on page action", async () => {
|
||||
(handleUrlFilters as Mock).mockReturnValue(true);
|
||||
(trackNoCodeAction as Mock).mockResolvedValue({ ok: true });
|
||||
|
||||
const mockConfig = createConfigWithTimeOnPageAction("dwellAction", 5);
|
||||
getInstanceConfigMock.mockReturnValue(mockConfig as unknown as Config);
|
||||
|
||||
const mockTimeoutStack = {
|
||||
getTimeouts: vi.fn().mockReturnValue([{ event: "dwellAction", timeoutId: 999 }]),
|
||||
remove: vi.fn(),
|
||||
add: vi.fn(),
|
||||
};
|
||||
getInstanceTimeoutStackMock.mockReturnValue(mockTimeoutStack as unknown as TimeoutStack);
|
||||
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://example.com/dashboard" },
|
||||
setTimeout: globalThis.setTimeout,
|
||||
});
|
||||
|
||||
await checkPageUrl();
|
||||
|
||||
(handleUrlFilters as Mock).mockReturnValue(false);
|
||||
await checkPageUrl();
|
||||
|
||||
expect(mockTimeoutStack.remove).toHaveBeenCalledWith(999);
|
||||
expect(setIsSurveyRunning).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +78,15 @@ export type TActionClassNoCodeConfig =
|
||||
rule: TActionClassPageUrlRule;
|
||||
}[];
|
||||
urlFiltersConnector?: "or" | "and";
|
||||
}
|
||||
| {
|
||||
type: "pageDwell";
|
||||
urlFilters: {
|
||||
value: string;
|
||||
rule: TActionClassPageUrlRule;
|
||||
}[];
|
||||
urlFiltersConnector?: "or" | "and";
|
||||
timeInSeconds: number;
|
||||
};
|
||||
|
||||
export interface TTrackProperties {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const ZActionClassPageUrlRule = z.enum(ACTION_CLASS_PAGE_URL_RULES);
|
||||
export type TActionClassPageUrlRule = z.infer<typeof ZActionClassPageUrlRule>;
|
||||
|
||||
const ZActionClassNoCodeConfigBase = z.object({
|
||||
type: z.enum(["click", "pageView", "exitIntent", "fiftyPercentScroll"]),
|
||||
type: z.enum(["click", "pageView", "exitIntent", "fiftyPercentScroll", "pageDwell"]),
|
||||
urlFilters: z.array(
|
||||
z.object({
|
||||
value: z.string().trim().min(1, {
|
||||
@@ -68,11 +68,17 @@ const ZActionClassNoCodeConfigfiftyPercentScroll = ZActionClassNoCodeConfigBase.
|
||||
type: z.literal("fiftyPercentScroll"),
|
||||
});
|
||||
|
||||
const ZActionClassNoCodeConfigTimeOnPage = ZActionClassNoCodeConfigBase.extend({
|
||||
type: z.literal("pageDwell"),
|
||||
timeInSeconds: z.number().int().min(1).max(3600),
|
||||
});
|
||||
|
||||
export const ZActionClassNoCodeConfig = z.union([
|
||||
ZActionClassNoCodeConfigClick,
|
||||
ZActionClassNoCodeConfigPageView,
|
||||
ZActionClassNoCodeConfigExitIntent,
|
||||
ZActionClassNoCodeConfigfiftyPercentScroll,
|
||||
ZActionClassNoCodeConfigTimeOnPage,
|
||||
]);
|
||||
|
||||
export type TActionClassNoCodeConfig = z.infer<typeof ZActionClassNoCodeConfig>;
|
||||
|
||||
Reference in New Issue
Block a user