From 9ad367b553721d295909bd351301b074b7a39b27 Mon Sep 17 00:00:00 2001 From: Lukas Boehler Date: Thu, 8 Oct 2026 14:29:29 +0200 Subject: [PATCH 1/2] feat: start surveys with the identified contact and close all-skipped surveys cleanly The messenger now skips survey contact questions an identified contact already answered (Messenger-App#240) and, when nothing is left to ask, closes the survey (survey-closed nothing_to_ask + close-widget) before it is shown. - showSurvey (manual, outbound triggers, links, survey pages) waits for an identify / updateContact in flight (at most 5 s), so the messenger reads the new contact when the survey starts. - A close-widget before the first-ping open no longer gets reopened 300 ms later as an empty, stuck "open" frame. A frame that was never shown hides without the close animation and without a close event. Co-Authored-By: Claude Opus 5.5 --- src/Gleap.js | 24 +++++++++------- src/GleapFrameManager.js | 23 ++++++++++++++-- src/GleapSession.js | 59 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/src/Gleap.js b/src/Gleap.js index 548c876d..fb38590c 100755 --- a/src/Gleap.js +++ b/src/Gleap.js @@ -1011,16 +1011,20 @@ class Gleap { return; } - const options = normalizeSurveyOptions(formatOrOptions); - Gleap.startFeedbackFlowWithOptions( - surveyId, - { - hideBackButton: true, - format: options.appMode, - survey: options, - }, - true - ); + // The messenger reads the contact once, when the survey starts, to skip the contact questions + // it already knows: wait for an identify / updateContact still in flight (capped). + sessionInstance.onContactSettled(() => { + const options = normalizeSurveyOptions(formatOrOptions); + Gleap.startFeedbackFlowWithOptions( + surveyId, + { + hideBackButton: true, + format: options.appMode, + survey: options, + }, + true + ); + }); } /** diff --git a/src/GleapFrameManager.js b/src/GleapFrameManager.js index db8ab648..bdd27698 100644 --- a/src/GleapFrameManager.js +++ b/src/GleapFrameManager.js @@ -52,6 +52,8 @@ export default class GleapFrameManager { // Card and page surveys stay invisible until the messenger reports their height (no tall blank flash). surveyAwaitingHeight = false; surveyRevealTimeout = null; + // A freshly injected frame opens shortly after its first ping, unless it was closed meanwhile. + openAfterPingTimeout = null; // The messenger fell back to a legacy (pre Surveys 2.0) survey: use the old survey chrome. surveyLegacy = false; // Surveys 2.0 corners reported by the messenger (survey-theme): 'sharp' | 'pill', null = rounded. @@ -539,6 +541,8 @@ export default class GleapFrameManager { clearTimeout(this.surveyRevealTimeout); this.surveyRevealTimeout = null; } + clearTimeout(this.openAfterPingTimeout); + this.openAfterPingTimeout = null; this.gleapFrameContainer = null; this.gleapFrame = null; this.captureHidden = false; @@ -950,6 +954,15 @@ export default class GleapFrameManager { return; } + // A survey the messenger closes before it was shown (nothing to ask): it must not open after + // all, and there is nothing to animate and no close event without an open. + const wasOpened = this.widgetOpened; + const wasVisible = this.isOpened(); + if (this.openAfterPingTimeout) { + clearTimeout(this.openAfterPingTimeout); + this.openAfterPingTimeout = null; + } + this.hideMarkerManager(); this.setCaptureEditorOpen(false); if (this.gleapFrameContainer) { @@ -966,7 +979,7 @@ export default class GleapFrameManager { this.surveyRevealTimeout = null; } - if (container.classList.contains('gleap-frame-container--measuring')) { + if (!wasOpened || container.classList.contains('gleap-frame-container--measuring')) { // Never shown: nothing to animate. container.classList.remove('gleap-frame-container--measuring'); container.classList.add('gleap-frame-container--hidden'); @@ -989,7 +1002,9 @@ export default class GleapFrameManager { this.widgetOpened = false; this.updateWidgetStatus(); GleapFeedbackButtonManager.getInstance().updateFeedbackButtonState(); - GleapEventManager.notifyEvent('close'); + if (wasVisible) { + GleapEventManager.notifyEvent('close'); + } GleapNotificationManager.getInstance().reloadNotificationsFromCache(); this.unregisterEscListener(); @@ -1073,7 +1088,9 @@ export default class GleapFrameManager { this.sendSessionUpdate(); this.sendCaptureCapabilities(); this.workThroughQueue(); - setTimeout(() => { + clearTimeout(this.openAfterPingTimeout); + this.openAfterPingTimeout = setTimeout(() => { + this.openAfterPingTimeout = null; this.runWidgetShouldOpenCallback(); }, 300); } diff --git a/src/GleapSession.js b/src/GleapSession.js index 290bcc21..38207a6f 100644 --- a/src/GleapSession.js +++ b/src/GleapSession.js @@ -50,6 +50,12 @@ export default class GleapSession { // never re-identified from stale state. lastIdentify = null; identifyInFlight = false; + // identify / updateContact requests still waiting for the server. A survey + // started meanwhile would get the previous contact (Surveys 2.0 skips the + // contact questions an identified contact already answered), so showSurvey + // waits for them (onContactSettled). + pendingContactUpdates = 0; + contactSettledListeners = []; // GleapSession singleton static instance; @@ -147,6 +153,51 @@ export default class GleapSession { } }; + /** + * Counts an identify / updateContact request until it settles. The session + * it returns is applied (validateSession, session-update to the messenger) + * before the listeners run. + */ + trackContactUpdate = (request) => { + this.pendingContactUpdates++; + const settle = () => { + this.pendingContactUpdates = Math.max(0, this.pendingContactUpdates - 1); + if (this.pendingContactUpdates > 0) { + return; + } + const listeners = this.contactSettledListeners; + this.contactSettledListeners = []; + for (let i = 0; i < listeners.length; i++) { + listeners[i](); + } + }; + request.then(settle, settle); + return request; + }; + + /** + * Runs `callback` once no identify / updateContact request is in flight, or + * after `timeoutMs` at the latest. + */ + onContactSettled = (callback, timeoutMs = 5000) => { + if (this.pendingContactUpdates === 0) { + callback(); + return; + } + let done = false; + let timer = null; + const run = () => { + if (done) { + return; + } + done = true; + clearTimeout(timer); + callback(); + }; + timer = setTimeout(run, timeoutMs); + this.contactSettledListeners.push(run); + }; + injectSession = (http) => { if (http && this.session) { http.setRequestHeader('Api-Token', this.sdkKey); @@ -477,7 +528,7 @@ export default class GleapSession { } const self = this; - return new Promise((resolve, reject) => { + return this.trackContactUpdate(new Promise((resolve, reject) => { // Wait for gleap session to be ready. this.setOnSessionReady(function () { if (!self.session.gleapId || !self.session.gleapHash) { @@ -526,7 +577,7 @@ export default class GleapSession { }) ); }); - }); + })); }; identifySession = (userId, userData, userHash, refreshFileSession = false) => { @@ -541,7 +592,7 @@ export default class GleapSession { } const self = this; - return new Promise((resolve, reject) => { + return this.trackContactUpdate(new Promise((resolve, reject) => { // Wait for gleap session to be ready. this.setOnSessionReady(function () { if (!self.session.gleapId || !self.session.gleapHash) { @@ -603,7 +654,7 @@ export default class GleapSession { }) ); }); - }); + })); }; startProductTourConfig = (tourId) => { From 9a66bd920e3a5efc1f504c5c5fb0298ccb780d4e Mon Sep 17 00:00:00 2001 From: Lukas Boehler Date: Thu, 8 Oct 2026 15:20:45 +0200 Subject: [PATCH 2/2] fix: a new survey card waits for its own height in a reused frame A card or page survey started in a frame that already measured an earlier survey card was revealed at once with the old height, so a survey with nothing to ask (the messenger now reports no height for it) flashed an empty card before it closed. Each new survey start now resets the height and waits for the survey's first height-update, or the 1.2 s fallback, like a cold frame. Co-Authored-By: Claude Opus 5.5 --- src/GleapFrameManager.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/GleapFrameManager.js b/src/GleapFrameManager.js index bdd27698..c2275543 100644 --- a/src/GleapFrameManager.js +++ b/src/GleapFrameManager.js @@ -156,8 +156,12 @@ export default class GleapFrameManager { this.surveyDark = null; this.updateFrameStyle(); - // Wait for the survey's height before showing a card/page survey in a frame that has none yet. - this.surveyAwaitingHeight = this.isAutoHeightSurvey() && (!this.comReady || !this.frameHeight); + // A card/page survey waits for its own height before it shows, also in a frame that already + // measured an earlier survey (the messenger reports none for a survey with nothing to ask). + if (this.isAutoHeightSurvey()) { + this.frameHeight = 0; + } + this.surveyAwaitingHeight = this.isAutoHeightSurvey(); this.applyInnerSize(); // Lay the hidden frame out (invisibly) so the messenger can measure the survey right away.