Skip to content

Commit d644b44

Browse files
authored
Merge pull request #180 from GleapSDK/fix-e2e-live-20261012
fix: live E2E fixes for the 10-12 release (capture pill, network log, no-CORS images, identity race)
2 parents 94b986b + 194e05f commit d644b44

7 files changed

Lines changed: 249 additions & 25 deletions

‎src/Gleap.js‎

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,15 @@ class Gleap {
10491049
* Starts the bug reporting flow.
10501050
*/
10511051
static startFeedbackFlowWithOptions(feedbackFlow, options = {}, isSurvey = false) {
1052+
// Right after Gleap.identify() (e.g. a user switch in the same browser) the previous contact's
1053+
// session is still active: the messenger would file the report for that contact. Start once the
1054+
// identify / updateContact in flight has landed (capped at 5 s; immediate when none is).
1055+
GleapSession.getInstance().onContactSettled(() => {
1056+
Gleap.startFeedbackFlowNow(feedbackFlow, options, isSurvey);
1057+
});
1058+
}
1059+
1060+
static startFeedbackFlowNow(feedbackFlow, options = {}, isSurvey = false) {
10521061
const { autostartDrawing, hideBackButton, format, survey } = options;
10531062
const sessionInstance = GleapSession.getInstance();
10541063
if (!sessionInstance.ready) {
@@ -1170,38 +1179,44 @@ class Gleap {
11701179
* Starts a new conversation and attaches the bot with the given id.
11711180
*/
11721181
static startBot(botId, showBackButton = true) {
1173-
GleapFrameManager.getInstance().setAppMode('widget');
1174-
GleapFrameManager.getInstance().sendMessage(
1175-
{
1176-
name: 'start-bot',
1177-
data: {
1178-
botId: botId ? botId : '',
1179-
hideBackButton: !showBackButton,
1182+
// Creates a conversation: wait for an identify still in flight (see startFeedbackFlowWithOptions).
1183+
GleapSession.getInstance().onContactSettled(() => {
1184+
GleapFrameManager.getInstance().setAppMode('widget');
1185+
GleapFrameManager.getInstance().sendMessage(
1186+
{
1187+
name: 'start-bot',
1188+
data: {
1189+
botId: botId ? botId : '',
1190+
hideBackButton: !showBackButton,
1191+
},
11801192
},
1181-
},
1182-
true
1183-
);
1193+
true
1194+
);
11841195

1185-
GleapFrameManager.getInstance().showWidget();
1196+
GleapFrameManager.getInstance().showWidget();
1197+
});
11861198
}
11871199

11881200
/**
11891201
* Starts a new conversation and attaches the bot with the given id.
11901202
*/
11911203
static askAI(question, showBackButton = true) {
1192-
GleapFrameManager.getInstance().setAppMode('widget');
1193-
GleapFrameManager.getInstance().sendMessage(
1194-
{
1195-
name: 'ask-ai',
1196-
data: {
1197-
question,
1198-
hideBackButton: !showBackButton,
1204+
// Creates a conversation: wait for an identify still in flight (see startFeedbackFlowWithOptions).
1205+
GleapSession.getInstance().onContactSettled(() => {
1206+
GleapFrameManager.getInstance().setAppMode('widget');
1207+
GleapFrameManager.getInstance().sendMessage(
1208+
{
1209+
name: 'ask-ai',
1210+
data: {
1211+
question,
1212+
hideBackButton: !showBackButton,
1213+
},
11991214
},
1200-
},
1201-
true
1202-
);
1215+
true
1216+
);
12031217

1204-
GleapFrameManager.getInstance().showWidget();
1218+
GleapFrameManager.getInstance().showWidget();
1219+
});
12051220
}
12061221

12071222
/**

‎src/GleapCaptureScreenshot.js‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ import { now, yieldToPage } from './GleapCaptureTasks';
1212
export const MAX_CAPTURE_EDGE = 2560;
1313
const JPEG_QUALITY = 0.85;
1414
const TRANSPARENT_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
15+
// An <img> the render can't download (cross-origin without CORS headers, so the canvas may not read
16+
// it) gets a neutral tint instead of an empty hole, so the screenshot doesn't look like the app lost
17+
// the image. Stretches to any box. The tab capture and the classic server render keep such images.
18+
const IMAGE_PLACEHOLDER =
19+
'data:image/svg+xml;charset=utf-8,' +
20+
encodeURIComponent(
21+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1" preserveAspectRatio="none">' +
22+
'<rect width="1" height="1" fill="rgb(128,128,128)" fill-opacity="0.16"/></svg>'
23+
);
24+
// CSS background images stay transparent (usually decoration).
25+
const imagePlaceholder = (imageDom) => (imageDom ? IMAGE_PLACEHOLDER : TRANSPARENT_GIF);
1526

1627
// Gleap's own UI never appears in a capture: the widget, launcher, notifications, banners, modals,
1728
// tooltips, tours, the admin helper and the capture UI itself.
@@ -1313,6 +1324,26 @@ const createImageFetcher = () => {
13131324
}
13141325
};
13151326

1327+
// The render downloads every image and font again (fetch). The logger is paused meanwhile, but the
1328+
// browser still lists those downloads in Resource Timing: mark them so they never show up as the
1329+
// app's requests (or failures) in the logs.
1330+
const ignoreInNetworkLogs = (url, since) => {
1331+
try {
1332+
if (typeof url === 'string' && url.indexOf('data:') !== 0 && url.indexOf('blob:') !== 0) {
1333+
GleapNetworkIntercepter.getInstance().ignoreInternalRequest(url, null, since);
1334+
}
1335+
} catch (exp) {}
1336+
};
1337+
1338+
// Wraps createImageFetcher's result: every image the render asks for is marked before the download.
1339+
const createTrackedImageFetcher = (since) => {
1340+
const fetchAway = createImageFetcher();
1341+
return (url) => {
1342+
ignoreInNetworkLogs(url, since);
1343+
return fetchAway ? fetchAway(url) : Promise.resolve(false);
1344+
};
1345+
};
1346+
13161347
// Network logging is paused while renders download assets; overlapping renders share one pause.
13171348
let networkLogPauses = 0;
13181349
let networkLogsWereStopped = false;
@@ -1374,6 +1405,7 @@ export const renderDom = ({ privacyOptions, maskSelectors, isActive }) =>
13741405

13751406
// Its asset downloads (fonts, images) are not the app's traffic: keep them out of the network logs.
13761407
const resumeNetworkLogs = pauseNetworkLogs();
1408+
const renderStart = now();
13771409
let context = null;
13781410
let sandbox = null;
13791411
let settled = false;
@@ -1383,6 +1415,12 @@ export const renderDom = ({ privacyOptions, maskSelectors, isActive }) =>
13831415
clearTimeout(deadline);
13841416
clearInterval(watch);
13851417
if (context) {
1418+
try {
1419+
// Fonts and CSS assets don't pass the image fetcher.
1420+
if (context.requests && typeof context.requests.forEach === 'function') {
1421+
context.requests.forEach((request, url) => ignoreInNetworkLogs(url, renderStart));
1422+
}
1423+
} catch (exp) {}
13861424
try {
13871425
// Removes the sandbox as well.
13881426
destroyContext(context);
@@ -1460,7 +1498,13 @@ export const renderDom = ({ privacyOptions, maskSelectors, isActive }) =>
14601498
overflow: 'visible',
14611499
},
14621500
features: { restoreScrollPosition: false },
1463-
fetchFn: createImageFetcher(),
1501+
fetchFn: createTrackedImageFetcher(renderStart),
1502+
// Passing `fetch` replaces modern-screenshot's defaults: keep its cache setting.
1503+
fetch: {
1504+
requestInit: { cache: 'force-cache' },
1505+
bypassingCache: false,
1506+
placeholderImage: imagePlaceholder,
1507+
},
14641508
// How long to wait for images that are still loading; the render itself gets longer below.
14651509
timeout: imagesLoadingInViewport() ? 3000 : 50,
14661510
});

‎src/GleapCaptureUI.js‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,9 @@ export default class GleapCaptureUI {
854854
this.dot.classList.toggle('paused', !!paused);
855855
}
856856
if (this.pausedLabel) {
857+
const before = this.pillRight();
857858
this.pausedLabel.style.display = paused ? '' : 'none';
859+
this.keepPillRight(before);
858860
}
859861
const button = this.pauseButton;
860862
if (button) {
@@ -865,6 +867,36 @@ export default class GleapCaptureUI {
865867
}
866868
}
867869

870+
pillRight() {
871+
try {
872+
return this.pill ? this.pill.getBoundingClientRect().right : null;
873+
} catch (exp) {
874+
return null;
875+
}
876+
}
877+
878+
// "Paused" widens the pill. A centred (or dragged) pill would grow to both sides and move Pause /
879+
// Resume away from the pointer that just clicked it, so the pill grows to the left instead.
880+
// Phones dock the pill across the whole width; nothing moves there.
881+
keepPillRight(before) {
882+
const pill = this.pill;
883+
const after = this.pillRight();
884+
if (!pill || before === null || after === null || window.innerWidth <= 600) {
885+
return;
886+
}
887+
const shift = after - before;
888+
if (Math.abs(shift) < 0.5) {
889+
return;
890+
}
891+
if (this.position) {
892+
this.position = { left: this.position.left - shift, top: this.position.top };
893+
this.place(pill);
894+
} else {
895+
const margin = (parseFloat(pill.style.marginLeft) || 0) - shift;
896+
pill.style.marginLeft = Math.abs(margin) < 0.5 ? '' : margin + 'px';
897+
}
898+
}
899+
868900
setMicOn(on) {
869901
const button = this.micButton;
870902
if (button) {

‎src/GleapNetworkIntercepter.js‎

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ const now = () => {
5858
return Date.now();
5959
};
6060

61+
// Internal urls remembered for Resource Timing filtering, and the clock slack for their entries.
62+
const MAX_INTERNAL_URLS = 300;
63+
const INTERNAL_URL_TOLERANCE_MS = 5;
64+
6165
const elapsedSince = (start) => Math.max(0, Math.round(now() - start));
6266

6367
const truncationMarker = (totalBytes) =>
@@ -506,6 +510,10 @@ class GleapNetworkIntercepter {
506510
initialized = false;
507511
stopped = false;
508512
loadAllResources = false;
513+
// The SDK's own downloads (screenshot assets): XHRs that are never recorded, and urls whose
514+
// Resource Timing entries from that moment on are left out.
515+
internalXhrs = typeof WeakSet !== 'undefined' ? new WeakSet() : null;
516+
internalUrls = new Map();
509517

510518
// GleapNetworkIntercepter singleton
511519
static instance;
@@ -516,6 +524,38 @@ class GleapNetworkIntercepter {
516524
return this.instance;
517525
}
518526

527+
/**
528+
* Keeps one of the SDK's own requests out of the network logs (e.g. the images a screenshot
529+
* downloads), so a failing asset download never reads as the app's failed request.
530+
* @param {string} url
531+
* @param {XMLHttpRequest} [xhr] call before xhr.open(); that request is not recorded
532+
* @param {number} [since] performance.now() of the first download; defaults to now
533+
*/
534+
ignoreInternalRequest(url, xhr, since) {
535+
try {
536+
if (xhr && this.internalXhrs) {
537+
this.internalXhrs.add(xhr);
538+
}
539+
if (!url) {
540+
return;
541+
}
542+
const key = resolveUrl(url);
543+
const start = typeof since === 'number' && isFinite(since) ? since : now();
544+
const previous = this.internalUrls.get(key);
545+
this.internalUrls.delete(key);
546+
this.internalUrls.set(key, typeof previous === 'number' ? Math.min(previous, start) : start);
547+
// Bounded: drop the oldest marks.
548+
while (this.internalUrls.size > MAX_INTERNAL_URLS) {
549+
this.internalUrls.delete(this.internalUrls.keys().next().value);
550+
}
551+
} catch (exp) {}
552+
}
553+
554+
isInternalResource(resource) {
555+
const since = this.internalUrls.get(resource.name);
556+
return typeof since === 'number' && resource.startTime >= since - INTERNAL_URL_TOLERANCE_MS;
557+
}
558+
519559
setLoadAllResources(loadAllResources) {
520560
this.loadAllResources = loadAllResources;
521561
}
@@ -603,7 +643,11 @@ class GleapNetworkIntercepter {
603643
if (!this.loadAllResources && ['xmlhttprequest', 'fetch'].indexOf(resource.initiatorType) === -1) {
604644
continue;
605645
}
606-
if (capturedUrls['url:' + resource.name] || isBlacklistedUrl(resource.name, blacklist)) {
646+
if (
647+
capturedUrls['url:' + resource.name] ||
648+
isBlacklistedUrl(resource.name, blacklist) ||
649+
this.isInternalResource(resource)
650+
) {
607651
continue;
608652
}
609653
const start = timeOrigin + resource.startTime;
@@ -941,7 +985,10 @@ class GleapNetworkIntercepter {
941985
state.method = String(method || 'GET').toUpperCase();
942986
state.url = resolveUrl(url);
943987
state.headers = {};
944-
state.capture = this.isCapturing() && !isBlacklistedUrl(state.url, this.getBlacklist());
988+
state.capture =
989+
this.isCapturing() &&
990+
!(this.internalXhrs && this.internalXhrs.has(xhr)) &&
991+
!isBlacklistedUrl(state.url, this.getBlacklist());
945992

946993
if (state.capture && !state.listening) {
947994
state.listening = true;

‎src/GleapSession.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export default class GleapSession {
5656
// waits for them (onContactSettled).
5757
pendingContactUpdates = 0;
5858
contactSettledListeners = [];
59+
// Bumped whenever an identify / updateContact result (or a logout) replaces the session. A
60+
// /sessions response for a request sent before that describes the previous contact and is dropped.
61+
contactVersion = 0;
5962

6063
// GleapSession singleton
6164
static instance;
@@ -245,6 +248,7 @@ export default class GleapSession {
245248
// replay state so the next session can't inherit a stale identity.
246249
this.lastIdentify = null;
247250
this.identifyInFlight = false;
251+
this.contactVersion++;
248252

249253
GleapFrameManager.getInstance().sendMessage(
250254
{
@@ -427,6 +431,7 @@ export default class GleapSession {
427431
}
428432

429433
const self = this;
434+
const contactVersionAtSend = this.contactVersion;
430435
const http = new XMLHttpRequest();
431436
http.open('POST', self.apiUrl + '/sessions');
432437
http.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
@@ -439,6 +444,12 @@ export default class GleapSession {
439444
} catch (exp) {}
440445
http.onreadystatechange = function (e) {
441446
if (http.readyState === 4) {
447+
// Gleap.identify() is sent right away against a cached session, so its answer (the new
448+
// contact) can land first. This older answer must not switch back to the previous contact
449+
// (or clear the new one).
450+
if (self.contactVersion !== contactVersionAtSend) {
451+
return;
452+
}
442453
if (http.status === 200 || http.status === 201) {
443454
try {
444455
const sessionData = JSON.parse(http.responseText);
@@ -552,6 +563,7 @@ export default class GleapSession {
552563
if (http.status === 200 || http.status === 201) {
553564
try {
554565
const sessionData = JSON.parse(http.responseText);
566+
self.contactVersion++;
555567
self.validateSession(sessionData);
556568
resolve(sessionData);
557569
} catch (exp) {
@@ -620,6 +632,7 @@ export default class GleapSession {
620632
if (http.status === 200 || http.status === 201) {
621633
try {
622634
const sessionData = JSON.parse(http.responseText);
635+
self.contactVersion++;
623636
self.validateSession(sessionData);
624637
resolve(sessionData);
625638
} catch (exp) {

0 commit comments

Comments
 (0)