@pendo/agent 2.339.1 → 2.341.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dom.esm.js +47 -3
- package/dist/pendo.module.js +831 -328
- package/dist/pendo.module.min.js +17 -17
- package/dist/servers.json +10 -10
- package/package.json +1 -1
package/dist/pendo.module.js
CHANGED
|
@@ -4046,8 +4046,8 @@ let SERVER = '';
|
|
|
4046
4046
|
let ASSET_HOST = '';
|
|
4047
4047
|
let ASSET_PATH = '';
|
|
4048
4048
|
let DESIGNER_SERVER = '';
|
|
4049
|
-
let VERSION = '2.
|
|
4050
|
-
let PACKAGE_VERSION = '2.
|
|
4049
|
+
let VERSION = '2.341.0_';
|
|
4050
|
+
let PACKAGE_VERSION = '2.341.0';
|
|
4051
4051
|
let LOADER = 'xhr';
|
|
4052
4052
|
/* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
|
|
4053
4053
|
/**
|
|
@@ -4464,6 +4464,135 @@ class LogEmitter extends EventTarget {
|
|
|
4464
4464
|
}
|
|
4465
4465
|
const log = new LogEmitter();
|
|
4466
4466
|
|
|
4467
|
+
/** Key used to lookup the consent collection policy in the pendo configuration. */
|
|
4468
|
+
const CONSENT_COLLECTION = 'cookieConsentCollection';
|
|
4469
|
+
/** Key used to lookup the analytics events in the consent collection policy. */
|
|
4470
|
+
const ANALYTICS = 'analytics';
|
|
4471
|
+
/** Key used to lookup the guide analytics events in the consent collection policy. */
|
|
4472
|
+
const GUIDE_ANALYTICS = 'guideAnalytics';
|
|
4473
|
+
/** Key used to lookup the text collection mode in the consent collection policy. */
|
|
4474
|
+
const TEXT_COLLECTION = 'textCollection';
|
|
4475
|
+
const TEXT_COLLECTION_LEVELS = ['none', 'whitelist', 'all'];
|
|
4476
|
+
let enabledProducts = null;
|
|
4477
|
+
function getClassification() {
|
|
4478
|
+
return ConfigReader.get(CONSENT_COLLECTION) || {};
|
|
4479
|
+
}
|
|
4480
|
+
function toTierList(tiers) {
|
|
4481
|
+
if (_.isString(tiers))
|
|
4482
|
+
return [tiers];
|
|
4483
|
+
return _.isArray(tiers) ? tiers : [];
|
|
4484
|
+
}
|
|
4485
|
+
function warnOnUnconfiguredTiers(grantedTiers, classification) {
|
|
4486
|
+
_.each(grantedTiers, function (tier) {
|
|
4487
|
+
if (_.isUndefined(classification[tier])) {
|
|
4488
|
+
log.warn('Consent tier "' + tier + '" is not present in ' + CONSENT_COLLECTION + '.');
|
|
4489
|
+
}
|
|
4490
|
+
});
|
|
4491
|
+
}
|
|
4492
|
+
function moreTextPermissive(a, b) {
|
|
4493
|
+
return TEXT_COLLECTION_LEVELS.indexOf(b) > TEXT_COLLECTION_LEVELS.indexOf(a) ? b : a;
|
|
4494
|
+
}
|
|
4495
|
+
function orBoolean(a, b) {
|
|
4496
|
+
return a || Boolean(b);
|
|
4497
|
+
}
|
|
4498
|
+
function unionArrays(a, b) {
|
|
4499
|
+
return _.union(a, b);
|
|
4500
|
+
}
|
|
4501
|
+
// How a granted tier's value for a product combines with what's been resolved so far.
|
|
4502
|
+
// `initial` doubles as what an ungranted tier contributes -- an unlisted product falls
|
|
4503
|
+
// back to DEFAULT_RESOLVER, the plain "any granted tier turns it on" boolean rule.
|
|
4504
|
+
const RESOLVERS = {
|
|
4505
|
+
[TEXT_COLLECTION]: { initial: TEXT_COLLECTION_LEVELS[0], combine: moreTextPermissive },
|
|
4506
|
+
[ANALYTICS]: { initial: [], combine: unionArrays },
|
|
4507
|
+
[GUIDE_ANALYTICS]: { initial: [], combine: unionArrays }
|
|
4508
|
+
};
|
|
4509
|
+
const DEFAULT_RESOLVER = { initial: false, combine: orBoolean };
|
|
4510
|
+
/**
|
|
4511
|
+
* Resolves the granted tiers against the configured product classification.
|
|
4512
|
+
*
|
|
4513
|
+
* A product the classification mentions anywhere starts at its resolver's `initial` value
|
|
4514
|
+
* -- a tier the visitor hasn't opted into contributes that same `initial` value, so it has
|
|
4515
|
+
* no say over the product, even to explicitly allow something. Only a granted tier's actual
|
|
4516
|
+
* value is combined in, so opting into an additional tier only ever adds capability. Most
|
|
4517
|
+
* products are plain booleans (DEFAULT_RESOLVER); `textCollection`, `analytics`, and
|
|
4518
|
+
* `guideAnalytics` have their own entries in RESOLVERS since none of them is a boolean.
|
|
4519
|
+
*
|
|
4520
|
+
* @access private
|
|
4521
|
+
* @param {string | string[]} tiers tier or tiers the visitor consented to
|
|
4522
|
+
* @returns {object} map of product to its resolved value
|
|
4523
|
+
*/
|
|
4524
|
+
function applyConsentTiers(tiers) {
|
|
4525
|
+
const cookieConsentCollection = getClassification();
|
|
4526
|
+
const consentedCookieTiers = toTierList(tiers);
|
|
4527
|
+
const resolved = {};
|
|
4528
|
+
warnOnUnconfiguredTiers(consentedCookieTiers, cookieConsentCollection);
|
|
4529
|
+
_.each(cookieConsentCollection, function (products, tier) {
|
|
4530
|
+
var isGranted = _.contains(consentedCookieTiers, tier);
|
|
4531
|
+
_.each(products, function (level, product) {
|
|
4532
|
+
var resolver = RESOLVERS[product] || DEFAULT_RESOLVER;
|
|
4533
|
+
var current = _.has(resolved, product) ? resolved[product] : resolver.initial;
|
|
4534
|
+
var contributed = isGranted ? level : resolver.initial;
|
|
4535
|
+
resolved[product] = resolver.combine(current, contributed);
|
|
4536
|
+
});
|
|
4537
|
+
});
|
|
4538
|
+
enabledProducts = resolved;
|
|
4539
|
+
return getEnabledProducts();
|
|
4540
|
+
}
|
|
4541
|
+
function getEnabledProducts() {
|
|
4542
|
+
return enabledProducts ? _.extend({}, enabledProducts) : null;
|
|
4543
|
+
}
|
|
4544
|
+
/**
|
|
4545
|
+
* A product is enabled unless a tier has explicitly resolved it to false -- with no cookie
|
|
4546
|
+
* consent configured at all, or a product no tier ever mentions, there's nothing to deny.
|
|
4547
|
+
*
|
|
4548
|
+
* @access private
|
|
4549
|
+
* @param {string} product
|
|
4550
|
+
* @returns {boolean}
|
|
4551
|
+
*/
|
|
4552
|
+
function isProductEnabled(product) {
|
|
4553
|
+
if (!enabledProducts || _.isUndefined(enabledProducts[product]))
|
|
4554
|
+
return true;
|
|
4555
|
+
return enabledProducts[product] === true;
|
|
4556
|
+
}
|
|
4557
|
+
function isTrackingConsentDenied() {
|
|
4558
|
+
return !isProductEnabled('identity');
|
|
4559
|
+
}
|
|
4560
|
+
/**
|
|
4561
|
+
* Session replay is denied whenever tracking itself is denied (DNT) -- a visitor
|
|
4562
|
+
* who can't be identified shouldn't have their session recorded either -- or when a
|
|
4563
|
+
* tier explicitly denies the `replay` product on its own.
|
|
4564
|
+
*
|
|
4565
|
+
* @access private
|
|
4566
|
+
* @returns {boolean}
|
|
4567
|
+
*/
|
|
4568
|
+
function isReplayConsentDenied() {
|
|
4569
|
+
return isTrackingConsentDenied() || !isProductEnabled('replay');
|
|
4570
|
+
}
|
|
4571
|
+
/**
|
|
4572
|
+
* Returns the consented text collection mode of the visitor.
|
|
4573
|
+
*
|
|
4574
|
+
* @access private
|
|
4575
|
+
* @returns {string} 'all', 'whitelist', or 'none'
|
|
4576
|
+
*/
|
|
4577
|
+
function getTextCollectionMode() {
|
|
4578
|
+
if (!enabledProducts)
|
|
4579
|
+
return 'all';
|
|
4580
|
+
var mode = enabledProducts[TEXT_COLLECTION];
|
|
4581
|
+
return _.isUndefined(mode) ? 'all' : mode;
|
|
4582
|
+
}
|
|
4583
|
+
function hasConsentForProduct(type, product) {
|
|
4584
|
+
if (!enabledProducts)
|
|
4585
|
+
return true;
|
|
4586
|
+
var allowed = enabledProducts[product];
|
|
4587
|
+
return _.isUndefined(allowed) ? true : _.contains(allowed, type);
|
|
4588
|
+
}
|
|
4589
|
+
function hasConsentForAnalyticsEvent(type) {
|
|
4590
|
+
return !isTrackingConsentDenied() && hasConsentForProduct(type, ANALYTICS);
|
|
4591
|
+
}
|
|
4592
|
+
function hasConsentForGuideEvent(type) {
|
|
4593
|
+
return hasConsentForProduct(type, GUIDE_ANALYTICS);
|
|
4594
|
+
}
|
|
4595
|
+
|
|
4467
4596
|
const allowKeys = function () {
|
|
4468
4597
|
var allowKeys = ConfigReader.get('storage.allowKeys') || '*';
|
|
4469
4598
|
return _.isArray(allowKeys) ? _.indexBy(allowKeys) : allowKeys;
|
|
@@ -4529,7 +4658,7 @@ function createStorageShim(storageAccessor) {
|
|
|
4529
4658
|
}
|
|
4530
4659
|
}
|
|
4531
4660
|
var pendoLocalStorage$3 = createStorageShim(function () { return window.localStorage; });
|
|
4532
|
-
var pendoSessionStorage$
|
|
4661
|
+
var pendoSessionStorage$3 = createStorageShim(function () { return window.sessionStorage; });
|
|
4533
4662
|
|
|
4534
4663
|
/**
|
|
4535
4664
|
* Safely deserializes JSON if value is a valid json string.
|
|
@@ -4556,9 +4685,11 @@ var areCookiesEnabled = true;
|
|
|
4556
4685
|
var allowLocalStorageOnly = function () {
|
|
4557
4686
|
return ConfigReader.get('localStorageOnly');
|
|
4558
4687
|
};
|
|
4559
|
-
|
|
4688
|
+
/*
|
|
4689
|
+
* This config setting really describes any persistence at all.
|
|
4690
|
+
*/
|
|
4560
4691
|
var storageIsDisabled = function () {
|
|
4561
|
-
return !!ConfigReader.get('disableCookies');
|
|
4692
|
+
return !!ConfigReader.get('disableCookies') || isTrackingConsentDenied();
|
|
4562
4693
|
};
|
|
4563
4694
|
function enableCookies(enabled) {
|
|
4564
4695
|
areCookiesEnabled = enabled;
|
|
@@ -4914,7 +5045,7 @@ var agentStorage = (function () {
|
|
|
4914
5045
|
}
|
|
4915
5046
|
});
|
|
4916
5047
|
}
|
|
4917
|
-
const wrappedPendoSessionStorage = _.extend({}, pendoSessionStorage$
|
|
5048
|
+
const wrappedPendoSessionStorage = _.extend({}, pendoSessionStorage$3);
|
|
4918
5049
|
wrappedPendoSessionStorage.setItem = wrapStorageWriteMethod(wrappedPendoSessionStorage.setItem, 'hasSession');
|
|
4919
5050
|
const wrappedPendoLocalStorage = _.extend({}, pendoLocalStorage$3);
|
|
4920
5051
|
wrappedPendoLocalStorage.setItem = wrapStorageWriteMethod(wrappedPendoLocalStorage.setItem, 'hasLocal');
|
|
@@ -5501,7 +5632,7 @@ else {
|
|
|
5501
5632
|
};
|
|
5502
5633
|
}
|
|
5503
5634
|
|
|
5504
|
-
const pendoSessionStorage$
|
|
5635
|
+
const pendoSessionStorage$2 = agentStorage.getSession();
|
|
5505
5636
|
/*
|
|
5506
5637
|
* EventTracer.addTracerIds - Returns a new copy of the original object with
|
|
5507
5638
|
* additional, contextual IDs added that provide a means of tracing event output
|
|
@@ -5583,7 +5714,7 @@ function createEventTracer(window) {
|
|
|
5583
5714
|
return tabId;
|
|
5584
5715
|
}
|
|
5585
5716
|
function getSessionStorage() {
|
|
5586
|
-
return sessionStorageMemoryCache(MemoryStorage, pendoSessionStorage$
|
|
5717
|
+
return sessionStorageMemoryCache(MemoryStorage, pendoSessionStorage$2);
|
|
5587
5718
|
}
|
|
5588
5719
|
function tabIdChanged(callback) {
|
|
5589
5720
|
if (!sniffer.addEventListener)
|
|
@@ -5771,6 +5902,8 @@ var Events = (function () {
|
|
|
5771
5902
|
new EventType('ptm:paused', [LIFECYCLE]),
|
|
5772
5903
|
new EventType('recording:unpaused', [LIFECYCLE]),
|
|
5773
5904
|
new EventType('recording:paused', [LIFECYCLE]),
|
|
5905
|
+
new EventType('consent:changed', [LIFECYCLE]),
|
|
5906
|
+
new EventType('identifyRequested', [DEBUG, LIFECYCLE]),
|
|
5774
5907
|
new EventType('childFrameJoined', [FRAMES]),
|
|
5775
5908
|
new EventType('tabIdChanged:self', [DEBUG, LIFECYCLE])
|
|
5776
5909
|
];
|
|
@@ -5987,6 +6120,7 @@ var SIZE_UNIQUE_ID = 11;
|
|
|
5987
6120
|
const VISITOR_ID_KEY = 'visitorId';
|
|
5988
6121
|
const OLD_VISITOR_ID_KEY = 'oldVisitorId';
|
|
5989
6122
|
const ACCOUNT_ID_KEY = 'accountId';
|
|
6123
|
+
const COOKIELESS_VISITOR_ID = '__cookieless_visitor__';
|
|
5990
6124
|
/**
|
|
5991
6125
|
* Generate a unique id string, usually for a visitor id. Default for anonymous visitor ids.
|
|
5992
6126
|
*
|
|
@@ -6038,6 +6172,9 @@ var DEFAULT_VISITOR_ID = 'VISITOR-UNIQUE-ID';
|
|
|
6038
6172
|
var isDefaultVisitor = function (vId) {
|
|
6039
6173
|
return DEFAULT_VISITOR_ID === vId;
|
|
6040
6174
|
};
|
|
6175
|
+
var isVisitorDNT = function (vId = pendo$1.visitorId) {
|
|
6176
|
+
return vId === COOKIELESS_VISITOR_ID;
|
|
6177
|
+
};
|
|
6041
6178
|
var SUBACCOUNT_DELIMITER = '::';
|
|
6042
6179
|
var isSubaccount = function (aId) {
|
|
6043
6180
|
return new RegExp(SUBACCOUNT_DELIMITER).test(aId);
|
|
@@ -6085,6 +6222,9 @@ function generate_anonymous_visitor_id() {
|
|
|
6085
6222
|
return generate_unique_id(TEMP_PREFIX);
|
|
6086
6223
|
}
|
|
6087
6224
|
function persistVisitorId(visitorId) {
|
|
6225
|
+
if (isTrackingConsentDenied()) {
|
|
6226
|
+
visitorId = COOKIELESS_VISITOR_ID;
|
|
6227
|
+
}
|
|
6088
6228
|
if (pendo$1.visitorId === visitorId)
|
|
6089
6229
|
return visitorId;
|
|
6090
6230
|
if (shouldPersist()) {
|
|
@@ -6153,6 +6293,10 @@ var set_visitor_id = function (new_visitor_id) {
|
|
|
6153
6293
|
log.info('Invalid visitor id ' + new_visitor_id);
|
|
6154
6294
|
return;
|
|
6155
6295
|
}
|
|
6296
|
+
if (isTrackingConsentDenied() && new_visitor_id !== COOKIELESS_VISITOR_ID) {
|
|
6297
|
+
log.warn('Ignoring visitor id ' + new_visitor_id + ' while identity consent is dnt.');
|
|
6298
|
+
return;
|
|
6299
|
+
}
|
|
6156
6300
|
persistVisitorId('' + new_visitor_id);
|
|
6157
6301
|
};
|
|
6158
6302
|
function set_old_visitor_id(new_old_visitor_id) {
|
|
@@ -6197,6 +6341,10 @@ var set_account_id = function (new_account_id) {
|
|
|
6197
6341
|
log.info('No account id provided');
|
|
6198
6342
|
return;
|
|
6199
6343
|
}
|
|
6344
|
+
if (isTrackingConsentDenied()) {
|
|
6345
|
+
log.warn('Ignoring account id ' + new_account_id + ' while identity consent is dnt.');
|
|
6346
|
+
return;
|
|
6347
|
+
}
|
|
6200
6348
|
if (!isValidId(new_account_id)) {
|
|
6201
6349
|
clearAccount();
|
|
6202
6350
|
log.info('Invalid account id: ' + new_account_id + '. Account cleared');
|
|
@@ -6239,10 +6387,12 @@ function clearAccount() {
|
|
|
6239
6387
|
*
|
|
6240
6388
|
* @access public
|
|
6241
6389
|
* @category Identity
|
|
6390
|
+
* @param {object} [options]
|
|
6391
|
+
* @param {boolean} [options.doNotTrack] register the visitor as DNT instead of a new anonymous visitor
|
|
6242
6392
|
* @example
|
|
6243
6393
|
* pendo.clearSession()
|
|
6244
6394
|
*/
|
|
6245
|
-
function clearSession() {
|
|
6395
|
+
function clearSession(options = { doNotTrack: false }) {
|
|
6246
6396
|
var props = {
|
|
6247
6397
|
old_visitor_id: get_visitor_id({ generate: false }),
|
|
6248
6398
|
old_account_id: get_account_id(),
|
|
@@ -6251,7 +6401,12 @@ function clearSession() {
|
|
|
6251
6401
|
removeIdentificationCookies();
|
|
6252
6402
|
delete pendo$1.visitorId;
|
|
6253
6403
|
delete pendo$1.accountId;
|
|
6254
|
-
|
|
6404
|
+
if (options.doNotTrack) {
|
|
6405
|
+
set_visitor_id(COOKIELESS_VISITOR_ID);
|
|
6406
|
+
}
|
|
6407
|
+
else {
|
|
6408
|
+
get_visitor_id({ generate: true });
|
|
6409
|
+
}
|
|
6255
6410
|
_.extend(props, {
|
|
6256
6411
|
visitor_id: get_visitor_id(),
|
|
6257
6412
|
account_id: get_account_id()
|
|
@@ -11072,6 +11227,9 @@ function wouldBeVisibleAfterAutoScroll(element) {
|
|
|
11072
11227
|
function scrollElementIntoParentRect(element, scrollParent) {
|
|
11073
11228
|
var clientRect = getClientRect(element);
|
|
11074
11229
|
var scrollRect = getClientRect(scrollParent);
|
|
11230
|
+
// A fixed element should already be visible so no need to scroll to it.
|
|
11231
|
+
if (clientRect.fixed && !scrollRect.fixed)
|
|
11232
|
+
return;
|
|
11075
11233
|
var yScrollAmount = 0;
|
|
11076
11234
|
var xScrollAmount = 0;
|
|
11077
11235
|
var diff;
|
|
@@ -13394,7 +13552,7 @@ function createSendQueue(options, send, guaranteedSend) {
|
|
|
13394
13552
|
}
|
|
13395
13553
|
function filterNonGuideAnalytics(beacon) {
|
|
13396
13554
|
return function (event, next) {
|
|
13397
|
-
var isGuidesOnly = ConfigReader.get('excludeNonGuideAnalytics');
|
|
13555
|
+
var isGuidesOnly = ConfigReader.get('excludeNonGuideAnalytics') || isTrackingConsentDenied();
|
|
13398
13556
|
if (beacon === 'ptm' && isGuidesOnly) {
|
|
13399
13557
|
return;
|
|
13400
13558
|
}
|
|
@@ -13403,7 +13561,7 @@ function filterNonGuideAnalytics(beacon) {
|
|
|
13403
13561
|
}
|
|
13404
13562
|
function filterAnalyticsByType(event, next) {
|
|
13405
13563
|
var excludeEvents = ConfigReader.get('analytics.excludeEvents');
|
|
13406
|
-
if (_.indexOf(excludeEvents, event.type) >= 0) {
|
|
13564
|
+
if (_.indexOf(excludeEvents, event.type) >= 0 || !hasConsentForAnalyticsEvent(event.type)) {
|
|
13407
13565
|
return;
|
|
13408
13566
|
}
|
|
13409
13567
|
next(event);
|
|
@@ -14039,13 +14197,15 @@ const internalEvents = {
|
|
|
14039
14197
|
onEventCaptured: 1,
|
|
14040
14198
|
segmentFlagsUpdated: 1,
|
|
14041
14199
|
segmentFlagsReady: 1,
|
|
14042
|
-
segmentFlagsError: 1
|
|
14200
|
+
segmentFlagsError: 1,
|
|
14201
|
+
identifyRequested: 1
|
|
14043
14202
|
};
|
|
14044
14203
|
const browserEvents = {
|
|
14045
14204
|
ready: 1,
|
|
14046
14205
|
segmentFlagsUpdated: 1,
|
|
14047
14206
|
segmentFlagsReady: 1,
|
|
14048
|
-
segmentFlagsError: 1
|
|
14207
|
+
segmentFlagsError: 1,
|
|
14208
|
+
identifyRequested: 1
|
|
14049
14209
|
};
|
|
14050
14210
|
const supportedPublicEvents = [
|
|
14051
14211
|
'ready',
|
|
@@ -14059,7 +14219,8 @@ const supportedPublicEvents = [
|
|
|
14059
14219
|
'segmentFlagsError',
|
|
14060
14220
|
'validateGuide',
|
|
14061
14221
|
'validateLauncher',
|
|
14062
|
-
'validateGlobalScript'
|
|
14222
|
+
'validateGlobalScript',
|
|
14223
|
+
'identifyRequested'
|
|
14063
14224
|
];
|
|
14064
14225
|
function forwardInternalEvents(Events) {
|
|
14065
14226
|
const unsubscribes = _.reduce(supportedPublicEvents, function (unsubscribes, eventName) {
|
|
@@ -14144,6 +14305,330 @@ var XHRGuideLoader = {
|
|
|
14144
14305
|
}
|
|
14145
14306
|
};
|
|
14146
14307
|
|
|
14308
|
+
function base32Encode(uint8array) {
|
|
14309
|
+
var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
14310
|
+
|
|
14311
|
+
var length = uint8array.length;
|
|
14312
|
+
|
|
14313
|
+
var bits = 0;
|
|
14314
|
+
var value = 0;
|
|
14315
|
+
var output = '';
|
|
14316
|
+
|
|
14317
|
+
for (var i = 0; i < length; i++) {
|
|
14318
|
+
value = (value << 8) | uint8array[i];
|
|
14319
|
+
bits += 8;
|
|
14320
|
+
|
|
14321
|
+
while (bits >= 5) {
|
|
14322
|
+
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
14323
|
+
bits -= 5;
|
|
14324
|
+
}
|
|
14325
|
+
}
|
|
14326
|
+
|
|
14327
|
+
if (bits > 0) {
|
|
14328
|
+
output += alphabet[(value << (5 - bits)) & 31];
|
|
14329
|
+
}
|
|
14330
|
+
|
|
14331
|
+
return output;
|
|
14332
|
+
}
|
|
14333
|
+
|
|
14334
|
+
// A CNAME contentHost is commonly configured without a scheme, which would leave the rewritten
|
|
14335
|
+
// url relative to the current page rather than absolute. Leading-slash values are deliberate and
|
|
14336
|
+
// kept as-is: '//host' is protocol-relative, '/path' is same-origin (see fixContentHostUrl).
|
|
14337
|
+
function normalizeContentHost(contentHost) {
|
|
14338
|
+
if (/:\/\//.test(contentHost) || /^\//.test(contentHost))
|
|
14339
|
+
return contentHost;
|
|
14340
|
+
return 'https://' + contentHost;
|
|
14341
|
+
}
|
|
14342
|
+
function stripScheme(host) {
|
|
14343
|
+
return host.replace(/^https?:\/\//, '');
|
|
14344
|
+
}
|
|
14345
|
+
// Self hosted installs aim assetHost at their own host, or drop it entirely - the npm package
|
|
14346
|
+
// deletes it for the assets option - so it cannot locate our CDN. SERVER survives either way, and
|
|
14347
|
+
// app, cdn and static are sibling labels in every environment's domain template. Builds that point
|
|
14348
|
+
// SERVER somewhere other than an app host (--env=local) are left with assetHost.
|
|
14349
|
+
function getPendoCdnHost() {
|
|
14350
|
+
var appLabel = /(^|\.)app\./;
|
|
14351
|
+
var serverHost = stripScheme(SERVER);
|
|
14352
|
+
if (!appLabel.test(serverHost))
|
|
14353
|
+
return stripScheme(ASSET_HOST);
|
|
14354
|
+
return serverHost.replace(appLabel, '$1cdn.');
|
|
14355
|
+
}
|
|
14356
|
+
// NOTE: this previously was *only* checking the snippet provided config for contentHost
|
|
14357
|
+
function replaceWithContentHost(str) {
|
|
14358
|
+
var contentHost = ConfigReader.getLocalConfig('contentHost');
|
|
14359
|
+
if (!contentHost || !str)
|
|
14360
|
+
return str;
|
|
14361
|
+
contentHost = normalizeContentHost(contentHost);
|
|
14362
|
+
var cdnHost = getPendoCdnHost();
|
|
14363
|
+
var assetHost = stripScheme(ASSET_HOST);
|
|
14364
|
+
// Content proxy hosts are the environment's domain with static in the variable label, which is
|
|
14365
|
+
// not always the leading one - us1 is content-<subId>.us1.static.pendo.io
|
|
14366
|
+
var staticHost = cdnHost.replace(/(^|\.)cdn\./, '$1static.');
|
|
14367
|
+
// An unknown host has to be dropped rather than interpolated: an empty one leaves a pattern
|
|
14368
|
+
// that matches the // of every absolute url
|
|
14369
|
+
var hostPatterns = [
|
|
14370
|
+
SUBSCRIPTION_BUCKET && `${escapeRegExp(SUBSCRIPTION_BUCKET)}-\\d+\\.storage\\.googleapis\\.com`,
|
|
14371
|
+
GLOBAL_BUCKET && `${escapeRegExp(GLOBAL_BUCKET)}\\.storage\\.googleapis\\.com`,
|
|
14372
|
+
cdnHost && `content-\\d+\\.${escapeRegExp(staticHost)}`,
|
|
14373
|
+
cdnHost && escapeRegExp(cdnHost),
|
|
14374
|
+
assetHost && assetHost !== cdnHost && escapeRegExp(assetHost)
|
|
14375
|
+
];
|
|
14376
|
+
return _.reduce(hostPatterns, function (rewritten, hostPattern) {
|
|
14377
|
+
if (!hostPattern)
|
|
14378
|
+
return rewritten;
|
|
14379
|
+
return rewritten.replace(new RegExp(`(https:)?\\/\\/${hostPattern}`, 'g'), contentHost);
|
|
14380
|
+
}, str);
|
|
14381
|
+
}
|
|
14382
|
+
function replaceObjectWithContentHost(obj) {
|
|
14383
|
+
var contentHost = ConfigReader.getLocalConfig('contentHost');
|
|
14384
|
+
if (!contentHost)
|
|
14385
|
+
return obj;
|
|
14386
|
+
return JSON.parse(replaceWithContentHost(JSON.stringify(obj)));
|
|
14387
|
+
}
|
|
14388
|
+
// APP-37205 we're only checking the remote contentHost config setting
|
|
14389
|
+
function getContentHostRegex() {
|
|
14390
|
+
var ch = escapeRegExp(ConfigReader.getHostedConfig('contentHost'));
|
|
14391
|
+
if (ch)
|
|
14392
|
+
return ch + '*';
|
|
14393
|
+
}
|
|
14394
|
+
/**
|
|
14395
|
+
* @access private
|
|
14396
|
+
* @param {Function} isValidGuideResponse
|
|
14397
|
+
* @param {object} guideResponse
|
|
14398
|
+
*/
|
|
14399
|
+
function isValidGuideResponse(guideResponse) {
|
|
14400
|
+
// Is it in proper JSON format/an object? If guideResponse does not have the 'type', 'props', or the 'children'
|
|
14401
|
+
// prop, we should also return false.
|
|
14402
|
+
if (!(_.isObject(guideResponse) && guideResponse.hasOwnProperty('type') &&
|
|
14403
|
+
guideResponse.hasOwnProperty('children') && guideResponse.hasOwnProperty('props'))) {
|
|
14404
|
+
return false;
|
|
14405
|
+
}
|
|
14406
|
+
return true;
|
|
14407
|
+
}
|
|
14408
|
+
var ContentLoader = (function (pendo) {
|
|
14409
|
+
var cache = {};
|
|
14410
|
+
// For APP-37205, we're verifing that the result is what it is. I know.
|
|
14411
|
+
var replaceWithContentHostAndVerify = _.compose(function (url) {
|
|
14412
|
+
var test = getContentHostRegex();
|
|
14413
|
+
if (test) {
|
|
14414
|
+
var v = new RegExp(test);
|
|
14415
|
+
if (!v.test(url)) {
|
|
14416
|
+
log.debug('Failed to verify URL: ' + url + ' vs ' + test);
|
|
14417
|
+
}
|
|
14418
|
+
}
|
|
14419
|
+
return url;
|
|
14420
|
+
}, replaceWithContentHost);
|
|
14421
|
+
/**
|
|
14422
|
+
* @access private
|
|
14423
|
+
* @typedef {object} ContentRequest
|
|
14424
|
+
* @param {string} id uniquely identifies the container
|
|
14425
|
+
* @param {string} contentUrl
|
|
14426
|
+
* @param {string} contentUrlCss
|
|
14427
|
+
* @param {string} contentUrlJs
|
|
14428
|
+
*/
|
|
14429
|
+
/**
|
|
14430
|
+
* @access private
|
|
14431
|
+
* @typedef {object} ContentEntry
|
|
14432
|
+
* @param {string} content The raw content as a string
|
|
14433
|
+
* @param {Function} template The template function compiled from the raw content
|
|
14434
|
+
* @param {Function} script The javascript to execute
|
|
14435
|
+
*/
|
|
14436
|
+
/**
|
|
14437
|
+
* Loads the content for the given container
|
|
14438
|
+
*
|
|
14439
|
+
* @access private
|
|
14440
|
+
* @param {ContentRequest} contentContainer
|
|
14441
|
+
* @returns {Promise.<ContentEntry>} Resolved when all of the content is loaded
|
|
14442
|
+
*/
|
|
14443
|
+
function load(contentContainer) {
|
|
14444
|
+
try {
|
|
14445
|
+
var cacheEntry = cache[contentContainer.id];
|
|
14446
|
+
if (cacheEntry && cacheEntry.language !== contentContainer.language) {
|
|
14447
|
+
delete cache[contentContainer.id];
|
|
14448
|
+
}
|
|
14449
|
+
if (!cache[contentContainer.id]) {
|
|
14450
|
+
var promises = [];
|
|
14451
|
+
var entry = {
|
|
14452
|
+
deferred: {},
|
|
14453
|
+
language: contentContainer.language
|
|
14454
|
+
};
|
|
14455
|
+
var fetchedDomContent = false;
|
|
14456
|
+
cache[contentContainer.id] = entry;
|
|
14457
|
+
if (XHRGuideLoader.usesXhr() && contentContainer.domUrl) {
|
|
14458
|
+
entry.deferred.domJson = q.defer();
|
|
14459
|
+
ajax.get(replaceWithContentHostAndVerify(contentContainer.domUrl)).then(function (result) {
|
|
14460
|
+
entry.domJson = replaceObjectWithContentHost(result.data);
|
|
14461
|
+
var guideResponse = result.data;
|
|
14462
|
+
if (isValidGuideResponse(guideResponse)) {
|
|
14463
|
+
entry.deferred.domJson.resolve();
|
|
14464
|
+
}
|
|
14465
|
+
else {
|
|
14466
|
+
// Log that the guide was invalid
|
|
14467
|
+
// handle promise
|
|
14468
|
+
log.critical(new Error('Response from the guide was not in proper JSON format'));
|
|
14469
|
+
entry.deferred.domJson.reject();
|
|
14470
|
+
}
|
|
14471
|
+
}).catch(function (e) {
|
|
14472
|
+
log.critical(e);
|
|
14473
|
+
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14474
|
+
entry.deferred.domJson.reject('Failed to load content');
|
|
14475
|
+
});
|
|
14476
|
+
promises.push(entry.deferred.domJson.promise);
|
|
14477
|
+
fetchedDomContent = true;
|
|
14478
|
+
}
|
|
14479
|
+
else if (contentContainer.domJsonpUrl) {
|
|
14480
|
+
entry.deferred.domJson = q.defer();
|
|
14481
|
+
var jsonpNode = loadResource(replaceWithContentHostAndVerify(contentContainer.domJsonpUrl), function (err) {
|
|
14482
|
+
dom.removeNode(jsonpNode);
|
|
14483
|
+
if (err) {
|
|
14484
|
+
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14485
|
+
return;
|
|
14486
|
+
}
|
|
14487
|
+
}, true);
|
|
14488
|
+
promises.push(entry.deferred.domJson.promise);
|
|
14489
|
+
fetchedDomContent = true;
|
|
14490
|
+
}
|
|
14491
|
+
if (contentContainer.contentUrlJs && !ConfigReader.get('preventCodeInjection')) {
|
|
14492
|
+
// Content, template, and script for 2.1+ agents
|
|
14493
|
+
entry.deferred.content = q.defer();
|
|
14494
|
+
var scriptNode = loadResource(replaceWithContentHostAndVerify(lightningRedirect(contentContainer.contentUrlJs)), function (err) {
|
|
14495
|
+
dom.removeNode(scriptNode);
|
|
14496
|
+
if (err) {
|
|
14497
|
+
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14498
|
+
return;
|
|
14499
|
+
}
|
|
14500
|
+
}, true);
|
|
14501
|
+
promises.push(entry.deferred.content.promise);
|
|
14502
|
+
// Style
|
|
14503
|
+
if (contentContainer.contentUrlCss) {
|
|
14504
|
+
entry.deferred.css = q.defer();
|
|
14505
|
+
loadResource({ url: replaceWithContentHostAndVerify(contentContainer.contentUrlCss), type: 'text/css' }, function () {
|
|
14506
|
+
entry.deferred.css.resolve();
|
|
14507
|
+
});
|
|
14508
|
+
promises.push(entry.deferred.css.promise);
|
|
14509
|
+
}
|
|
14510
|
+
}
|
|
14511
|
+
else if (contentContainer.contentUrl && !ConfigReader.get('preventCodeInjection')) {
|
|
14512
|
+
// Content with inlined script and css (2.0.x agents, or guides that have not migrated to the 2.1 format yet)
|
|
14513
|
+
entry.deferred.content = q.defer();
|
|
14514
|
+
var contentNode = loadResource(addExtensionToUrl(replaceWithContentHostAndVerify(contentContainer.contentUrl), '.js'), function (err) {
|
|
14515
|
+
dom.removeNode(contentNode);
|
|
14516
|
+
if (err) {
|
|
14517
|
+
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14518
|
+
return;
|
|
14519
|
+
}
|
|
14520
|
+
}, true);
|
|
14521
|
+
promises.push(entry.deferred.content.promise);
|
|
14522
|
+
}
|
|
14523
|
+
else if (!fetchedDomContent) {
|
|
14524
|
+
return q.reject('fetchedDomContent is still false');
|
|
14525
|
+
}
|
|
14526
|
+
entry.deferred.promise = q.all(promises).then(function () {
|
|
14527
|
+
return _.omit(entry, 'deferred');
|
|
14528
|
+
});
|
|
14529
|
+
}
|
|
14530
|
+
return cache[contentContainer.id].deferred.promise;
|
|
14531
|
+
}
|
|
14532
|
+
catch (e) {
|
|
14533
|
+
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14534
|
+
throw e;
|
|
14535
|
+
}
|
|
14536
|
+
}
|
|
14537
|
+
// Clears the content cache. Used for testing.
|
|
14538
|
+
function reset() {
|
|
14539
|
+
cache = {};
|
|
14540
|
+
}
|
|
14541
|
+
/**
|
|
14542
|
+
* JSONP-style callback for loading guide content
|
|
14543
|
+
* from script resources. Step content stored in
|
|
14544
|
+
* GCS is wrapped with pendo.guideContent(...)
|
|
14545
|
+
*
|
|
14546
|
+
* @access private
|
|
14547
|
+
* @param {string} guideId
|
|
14548
|
+
* @param {string} stepId
|
|
14549
|
+
* @param {string} content
|
|
14550
|
+
* @param {Function} templateFn
|
|
14551
|
+
* @param {Function} scriptFn
|
|
14552
|
+
*/
|
|
14553
|
+
function guideContent(guideId, stepId, content, templateFn, scriptFn) {
|
|
14554
|
+
if (_.isString(content)) {
|
|
14555
|
+
content = preprocessGuideContent(content, guideId, stepId);
|
|
14556
|
+
if (!_.isFunction(templateFn)) {
|
|
14557
|
+
if (shouldCreateTemplate(content)) {
|
|
14558
|
+
templateFn = _.template(content);
|
|
14559
|
+
}
|
|
14560
|
+
else {
|
|
14561
|
+
templateFn = _.constant(content);
|
|
14562
|
+
}
|
|
14563
|
+
}
|
|
14564
|
+
var entry = cache[guideId + stepId];
|
|
14565
|
+
if (entry && entry.deferred.content) {
|
|
14566
|
+
entry.content = content;
|
|
14567
|
+
entry.template = templateFn;
|
|
14568
|
+
entry.script = scriptFn;
|
|
14569
|
+
entry.deferred.content.resolve();
|
|
14570
|
+
}
|
|
14571
|
+
}
|
|
14572
|
+
}
|
|
14573
|
+
// preprocess content to remove simple guide and step IDs templating
|
|
14574
|
+
function removeStandardTemplating(content, guideId, stepId) {
|
|
14575
|
+
const stdTemplating = `<% if (typeof guide !== 'undefined') { %>
|
|
14576
|
+
var guide = pendo.findGuideById('<%= guide.id %>');
|
|
14577
|
+
var step = guide && guide.findStepById('<%= step.id %>');
|
|
14578
|
+
<% } %>`;
|
|
14579
|
+
const sansTemplating = `var guide = pendo.findGuideById('${guideId}');
|
|
14580
|
+
var step = guide && guide.findStepById('${stepId}');`;
|
|
14581
|
+
return content.replace(stdTemplating, sansTemplating);
|
|
14582
|
+
}
|
|
14583
|
+
// execute all content preprocessors and return end result
|
|
14584
|
+
const GuideContentProcessorList = [removeStandardTemplating];
|
|
14585
|
+
function preprocessGuideContent(content, guideId, stepId) {
|
|
14586
|
+
return _.reduce(GuideContentProcessorList, (acc, procFn) => {
|
|
14587
|
+
return procFn(acc, guideId, stepId);
|
|
14588
|
+
}, content);
|
|
14589
|
+
}
|
|
14590
|
+
function shouldCreateTemplate(content) {
|
|
14591
|
+
return /<%/.test(content) && /%>/.test(content);
|
|
14592
|
+
}
|
|
14593
|
+
function receiveDomStructureJson(guideId, stepId, domJson) {
|
|
14594
|
+
var entry = cache[guideId + stepId];
|
|
14595
|
+
if (entry && entry.deferred.domJson) {
|
|
14596
|
+
entry.domJson = replaceObjectWithContentHost(domJson);
|
|
14597
|
+
entry.deferred.domJson.resolve();
|
|
14598
|
+
}
|
|
14599
|
+
}
|
|
14600
|
+
function lightningRedirect(url) {
|
|
14601
|
+
if (isSfdcLightning()) {
|
|
14602
|
+
// https://pendo-static-1234.storage.googleapis.com/guide-content/:guideId/:stepId/:someKindaHash.guide.js
|
|
14603
|
+
var path = url.replace(/^https?:\/\/[^/]+\/guide-content\//, '').split('/');
|
|
14604
|
+
var guideId = path[0];
|
|
14605
|
+
/* global $A */
|
|
14606
|
+
var resourcePath = $A.get('$Resource.pendoGuide' + base32Encode(toUTF8Array(guideId)));
|
|
14607
|
+
if (_.isString(resourcePath)) {
|
|
14608
|
+
return resourcePath + '/' + path.join('/');
|
|
14609
|
+
}
|
|
14610
|
+
}
|
|
14611
|
+
return url;
|
|
14612
|
+
}
|
|
14613
|
+
function addExtensionToUrl(url, extension) {
|
|
14614
|
+
if (!_.isString(url))
|
|
14615
|
+
return url;
|
|
14616
|
+
var i = _.indexOf(url, '?');
|
|
14617
|
+
if (i < 0) {
|
|
14618
|
+
return url + extension;
|
|
14619
|
+
}
|
|
14620
|
+
else {
|
|
14621
|
+
return url.substring(0, i) + extension + url.substring(i);
|
|
14622
|
+
}
|
|
14623
|
+
}
|
|
14624
|
+
return {
|
|
14625
|
+
load,
|
|
14626
|
+
reset,
|
|
14627
|
+
guideContent,
|
|
14628
|
+
receiveDomStructureJson
|
|
14629
|
+
};
|
|
14630
|
+
})();
|
|
14631
|
+
|
|
14147
14632
|
function getHashFromContentUrl(url) {
|
|
14148
14633
|
var contentUrl = parseUrl(url);
|
|
14149
14634
|
var sha1Length = 27;
|
|
@@ -14230,10 +14715,12 @@ var ContentVerifier = (function () {
|
|
|
14230
14715
|
if (!_.isString(url))
|
|
14231
14716
|
return q.reject('unable to parse "' + url + '"');
|
|
14232
14717
|
var stepHash = computeStepHash(contentContainer, urlProp);
|
|
14233
|
-
|
|
14718
|
+
// Verify against the host the loader fetches from; the hash still comes from the original url.
|
|
14719
|
+
var fetchUrl = replaceWithContentHost(url);
|
|
14720
|
+
return ajax.get(fetchUrl).then(function (response) {
|
|
14234
14721
|
if (computeHashFromFile(response.data) === stepHash)
|
|
14235
14722
|
return;
|
|
14236
|
-
return q.reject('Unable to verify content at "' +
|
|
14723
|
+
return q.reject('Unable to verify content at "' + fetchUrl + '"');
|
|
14237
14724
|
});
|
|
14238
14725
|
}
|
|
14239
14726
|
function verifyContentHashUnlessSri(contentContainer, urlProp) {
|
|
@@ -14508,297 +14995,6 @@ function GuideStepFactory(step, guide) {
|
|
|
14508
14995
|
GuideFactory.behaviors = [];
|
|
14509
14996
|
GuideStepFactory.behaviors = [];
|
|
14510
14997
|
|
|
14511
|
-
function base32Encode(uint8array) {
|
|
14512
|
-
var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
14513
|
-
|
|
14514
|
-
var length = uint8array.length;
|
|
14515
|
-
|
|
14516
|
-
var bits = 0;
|
|
14517
|
-
var value = 0;
|
|
14518
|
-
var output = '';
|
|
14519
|
-
|
|
14520
|
-
for (var i = 0; i < length; i++) {
|
|
14521
|
-
value = (value << 8) | uint8array[i];
|
|
14522
|
-
bits += 8;
|
|
14523
|
-
|
|
14524
|
-
while (bits >= 5) {
|
|
14525
|
-
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
14526
|
-
bits -= 5;
|
|
14527
|
-
}
|
|
14528
|
-
}
|
|
14529
|
-
|
|
14530
|
-
if (bits > 0) {
|
|
14531
|
-
output += alphabet[(value << (5 - bits)) & 31];
|
|
14532
|
-
}
|
|
14533
|
-
|
|
14534
|
-
return output;
|
|
14535
|
-
}
|
|
14536
|
-
|
|
14537
|
-
// NOTE: this previously was *only* checking the snippet provided config for contentHost
|
|
14538
|
-
function replaceWithContentHost(str) {
|
|
14539
|
-
var contentHost = ConfigReader.getLocalConfig('contentHost');
|
|
14540
|
-
if (!contentHost || !str)
|
|
14541
|
-
return str;
|
|
14542
|
-
var subBucketPattern = new RegExp(`(https:)?\\/\\/${SUBSCRIPTION_BUCKET}-\\d+\\.storage\\.googleapis\\.com`, 'g');
|
|
14543
|
-
var globalBucketPattern = new RegExp(`(https:)?\\/\\/${GLOBAL_BUCKET}\\.storage\\.googleapis\\.com`, 'g');
|
|
14544
|
-
// Content proxy hosts look like content-<subId>.static.<envDomain>, where envDomain is ASSET_HOST minus its cdn. prefix
|
|
14545
|
-
var envDomain = ASSET_HOST.replace(/^cdn\./, '');
|
|
14546
|
-
var contentProxyPattern = new RegExp(`(https:)?\\/\\/content-\\d+\\.static\\.${escapeRegExp(envDomain)}`, 'g');
|
|
14547
|
-
return str.replace(subBucketPattern, contentHost)
|
|
14548
|
-
.replace(globalBucketPattern, contentHost)
|
|
14549
|
-
.replace(contentProxyPattern, contentHost)
|
|
14550
|
-
.replace(new RegExp(`(https:)?\\/\\/${escapeRegExp(ASSET_HOST)}`, 'g'), contentHost);
|
|
14551
|
-
}
|
|
14552
|
-
function replaceObjectWithContentHost(obj) {
|
|
14553
|
-
var contentHost = ConfigReader.getLocalConfig('contentHost');
|
|
14554
|
-
if (!contentHost)
|
|
14555
|
-
return obj;
|
|
14556
|
-
return JSON.parse(replaceWithContentHost(JSON.stringify(obj)));
|
|
14557
|
-
}
|
|
14558
|
-
// APP-37205 we're only checking the remote contentHost config setting
|
|
14559
|
-
function getContentHostRegex() {
|
|
14560
|
-
var ch = escapeRegExp(ConfigReader.getHostedConfig('contentHost'));
|
|
14561
|
-
if (ch)
|
|
14562
|
-
return ch + '*';
|
|
14563
|
-
}
|
|
14564
|
-
/**
|
|
14565
|
-
* @access private
|
|
14566
|
-
* @param {Function} isValidGuideResponse
|
|
14567
|
-
* @param {object} guideResponse
|
|
14568
|
-
*/
|
|
14569
|
-
function isValidGuideResponse(guideResponse) {
|
|
14570
|
-
// Is it in proper JSON format/an object? If guideResponse does not have the 'type', 'props', or the 'children'
|
|
14571
|
-
// prop, we should also return false.
|
|
14572
|
-
if (!(_.isObject(guideResponse) && guideResponse.hasOwnProperty('type') &&
|
|
14573
|
-
guideResponse.hasOwnProperty('children') && guideResponse.hasOwnProperty('props'))) {
|
|
14574
|
-
return false;
|
|
14575
|
-
}
|
|
14576
|
-
return true;
|
|
14577
|
-
}
|
|
14578
|
-
var ContentLoader = (function (pendo) {
|
|
14579
|
-
var cache = {};
|
|
14580
|
-
// For APP-37205, we're verifing that the result is what it is. I know.
|
|
14581
|
-
var replaceWithContentHostAndVerify = _.compose(function (url) {
|
|
14582
|
-
var test = getContentHostRegex();
|
|
14583
|
-
if (test) {
|
|
14584
|
-
var v = new RegExp(test);
|
|
14585
|
-
if (!v.test(url)) {
|
|
14586
|
-
log.debug('Failed to verify URL: ' + url + ' vs ' + test);
|
|
14587
|
-
}
|
|
14588
|
-
}
|
|
14589
|
-
return url;
|
|
14590
|
-
}, replaceWithContentHost);
|
|
14591
|
-
/**
|
|
14592
|
-
* @access private
|
|
14593
|
-
* @typedef {object} ContentRequest
|
|
14594
|
-
* @param {string} id uniquely identifies the container
|
|
14595
|
-
* @param {string} contentUrl
|
|
14596
|
-
* @param {string} contentUrlCss
|
|
14597
|
-
* @param {string} contentUrlJs
|
|
14598
|
-
*/
|
|
14599
|
-
/**
|
|
14600
|
-
* @access private
|
|
14601
|
-
* @typedef {object} ContentEntry
|
|
14602
|
-
* @param {string} content The raw content as a string
|
|
14603
|
-
* @param {Function} template The template function compiled from the raw content
|
|
14604
|
-
* @param {Function} script The javascript to execute
|
|
14605
|
-
*/
|
|
14606
|
-
/**
|
|
14607
|
-
* Loads the content for the given container
|
|
14608
|
-
*
|
|
14609
|
-
* @access private
|
|
14610
|
-
* @param {ContentRequest} contentContainer
|
|
14611
|
-
* @returns {Promise.<ContentEntry>} Resolved when all of the content is loaded
|
|
14612
|
-
*/
|
|
14613
|
-
function load(contentContainer) {
|
|
14614
|
-
try {
|
|
14615
|
-
var cacheEntry = cache[contentContainer.id];
|
|
14616
|
-
if (cacheEntry && cacheEntry.language !== contentContainer.language) {
|
|
14617
|
-
delete cache[contentContainer.id];
|
|
14618
|
-
}
|
|
14619
|
-
if (!cache[contentContainer.id]) {
|
|
14620
|
-
var promises = [];
|
|
14621
|
-
var entry = {
|
|
14622
|
-
deferred: {},
|
|
14623
|
-
language: contentContainer.language
|
|
14624
|
-
};
|
|
14625
|
-
var fetchedDomContent = false;
|
|
14626
|
-
cache[contentContainer.id] = entry;
|
|
14627
|
-
if (XHRGuideLoader.usesXhr() && contentContainer.domUrl) {
|
|
14628
|
-
entry.deferred.domJson = q.defer();
|
|
14629
|
-
ajax.get(replaceWithContentHostAndVerify(contentContainer.domUrl)).then(function (result) {
|
|
14630
|
-
entry.domJson = replaceObjectWithContentHost(result.data);
|
|
14631
|
-
var guideResponse = result.data;
|
|
14632
|
-
if (isValidGuideResponse(guideResponse)) {
|
|
14633
|
-
entry.deferred.domJson.resolve();
|
|
14634
|
-
}
|
|
14635
|
-
else {
|
|
14636
|
-
// Log that the guide was invalid
|
|
14637
|
-
// handle promise
|
|
14638
|
-
log.critical(new Error('Response from the guide was not in proper JSON format'));
|
|
14639
|
-
entry.deferred.domJson.reject();
|
|
14640
|
-
}
|
|
14641
|
-
}).catch(function (e) {
|
|
14642
|
-
log.critical(e);
|
|
14643
|
-
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14644
|
-
entry.deferred.domJson.reject('Failed to load content');
|
|
14645
|
-
});
|
|
14646
|
-
promises.push(entry.deferred.domJson.promise);
|
|
14647
|
-
fetchedDomContent = true;
|
|
14648
|
-
}
|
|
14649
|
-
else if (contentContainer.domJsonpUrl) {
|
|
14650
|
-
entry.deferred.domJson = q.defer();
|
|
14651
|
-
var jsonpNode = loadResource(replaceWithContentHostAndVerify(contentContainer.domJsonpUrl), function (err) {
|
|
14652
|
-
dom.removeNode(jsonpNode);
|
|
14653
|
-
if (err) {
|
|
14654
|
-
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14655
|
-
return;
|
|
14656
|
-
}
|
|
14657
|
-
}, true);
|
|
14658
|
-
promises.push(entry.deferred.domJson.promise);
|
|
14659
|
-
fetchedDomContent = true;
|
|
14660
|
-
}
|
|
14661
|
-
if (contentContainer.contentUrlJs && !ConfigReader.get('preventCodeInjection')) {
|
|
14662
|
-
// Content, template, and script for 2.1+ agents
|
|
14663
|
-
entry.deferred.content = q.defer();
|
|
14664
|
-
var scriptNode = loadResource(replaceWithContentHostAndVerify(lightningRedirect(contentContainer.contentUrlJs)), function (err) {
|
|
14665
|
-
dom.removeNode(scriptNode);
|
|
14666
|
-
if (err) {
|
|
14667
|
-
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14668
|
-
return;
|
|
14669
|
-
}
|
|
14670
|
-
}, true);
|
|
14671
|
-
promises.push(entry.deferred.content.promise);
|
|
14672
|
-
// Style
|
|
14673
|
-
if (contentContainer.contentUrlCss) {
|
|
14674
|
-
entry.deferred.css = q.defer();
|
|
14675
|
-
loadResource({ url: replaceWithContentHostAndVerify(contentContainer.contentUrlCss), type: 'text/css' }, function () {
|
|
14676
|
-
entry.deferred.css.resolve();
|
|
14677
|
-
});
|
|
14678
|
-
promises.push(entry.deferred.css.promise);
|
|
14679
|
-
}
|
|
14680
|
-
}
|
|
14681
|
-
else if (contentContainer.contentUrl && !ConfigReader.get('preventCodeInjection')) {
|
|
14682
|
-
// Content with inlined script and css (2.0.x agents, or guides that have not migrated to the 2.1 format yet)
|
|
14683
|
-
entry.deferred.content = q.defer();
|
|
14684
|
-
var contentNode = loadResource(addExtensionToUrl(replaceWithContentHostAndVerify(contentContainer.contentUrl), '.js'), function (err) {
|
|
14685
|
-
dom.removeNode(contentNode);
|
|
14686
|
-
if (err) {
|
|
14687
|
-
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14688
|
-
return;
|
|
14689
|
-
}
|
|
14690
|
-
}, true);
|
|
14691
|
-
promises.push(entry.deferred.content.promise);
|
|
14692
|
-
}
|
|
14693
|
-
else if (!fetchedDomContent) {
|
|
14694
|
-
return q.reject('fetchedDomContent is still false');
|
|
14695
|
-
}
|
|
14696
|
-
entry.deferred.promise = q.all(promises).then(function () {
|
|
14697
|
-
return _.omit(entry, 'deferred');
|
|
14698
|
-
});
|
|
14699
|
-
}
|
|
14700
|
-
return cache[contentContainer.id].deferred.promise;
|
|
14701
|
-
}
|
|
14702
|
-
catch (e) {
|
|
14703
|
-
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14704
|
-
throw e;
|
|
14705
|
-
}
|
|
14706
|
-
}
|
|
14707
|
-
// Clears the content cache. Used for testing.
|
|
14708
|
-
function reset() {
|
|
14709
|
-
cache = {};
|
|
14710
|
-
}
|
|
14711
|
-
/**
|
|
14712
|
-
* JSONP-style callback for loading guide content
|
|
14713
|
-
* from script resources. Step content stored in
|
|
14714
|
-
* GCS is wrapped with pendo.guideContent(...)
|
|
14715
|
-
*
|
|
14716
|
-
* @access private
|
|
14717
|
-
* @param {string} guideId
|
|
14718
|
-
* @param {string} stepId
|
|
14719
|
-
* @param {string} content
|
|
14720
|
-
* @param {Function} templateFn
|
|
14721
|
-
* @param {Function} scriptFn
|
|
14722
|
-
*/
|
|
14723
|
-
function guideContent(guideId, stepId, content, templateFn, scriptFn) {
|
|
14724
|
-
if (_.isString(content)) {
|
|
14725
|
-
content = preprocessGuideContent(content, guideId, stepId);
|
|
14726
|
-
if (!_.isFunction(templateFn)) {
|
|
14727
|
-
if (shouldCreateTemplate(content)) {
|
|
14728
|
-
templateFn = _.template(content);
|
|
14729
|
-
}
|
|
14730
|
-
else {
|
|
14731
|
-
templateFn = _.constant(content);
|
|
14732
|
-
}
|
|
14733
|
-
}
|
|
14734
|
-
var entry = cache[guideId + stepId];
|
|
14735
|
-
if (entry && entry.deferred.content) {
|
|
14736
|
-
entry.content = content;
|
|
14737
|
-
entry.template = templateFn;
|
|
14738
|
-
entry.script = scriptFn;
|
|
14739
|
-
entry.deferred.content.resolve();
|
|
14740
|
-
}
|
|
14741
|
-
}
|
|
14742
|
-
}
|
|
14743
|
-
// preprocess content to remove simple guide and step IDs templating
|
|
14744
|
-
function removeStandardTemplating(content, guideId, stepId) {
|
|
14745
|
-
const stdTemplating = `<% if (typeof guide !== 'undefined') { %>
|
|
14746
|
-
var guide = pendo.findGuideById('<%= guide.id %>');
|
|
14747
|
-
var step = guide && guide.findStepById('<%= step.id %>');
|
|
14748
|
-
<% } %>`;
|
|
14749
|
-
const sansTemplating = `var guide = pendo.findGuideById('${guideId}');
|
|
14750
|
-
var step = guide && guide.findStepById('${stepId}');`;
|
|
14751
|
-
return content.replace(stdTemplating, sansTemplating);
|
|
14752
|
-
}
|
|
14753
|
-
// execute all content preprocessors and return end result
|
|
14754
|
-
const GuideContentProcessorList = [removeStandardTemplating];
|
|
14755
|
-
function preprocessGuideContent(content, guideId, stepId) {
|
|
14756
|
-
return _.reduce(GuideContentProcessorList, (acc, procFn) => {
|
|
14757
|
-
return procFn(acc, guideId, stepId);
|
|
14758
|
-
}, content);
|
|
14759
|
-
}
|
|
14760
|
-
function shouldCreateTemplate(content) {
|
|
14761
|
-
return /<%/.test(content) && /%>/.test(content);
|
|
14762
|
-
}
|
|
14763
|
-
function receiveDomStructureJson(guideId, stepId, domJson) {
|
|
14764
|
-
var entry = cache[guideId + stepId];
|
|
14765
|
-
if (entry && entry.deferred.domJson) {
|
|
14766
|
-
entry.domJson = replaceObjectWithContentHost(domJson);
|
|
14767
|
-
entry.deferred.domJson.resolve();
|
|
14768
|
-
}
|
|
14769
|
-
}
|
|
14770
|
-
function lightningRedirect(url) {
|
|
14771
|
-
if (isSfdcLightning()) {
|
|
14772
|
-
// https://pendo-static-1234.storage.googleapis.com/guide-content/:guideId/:stepId/:someKindaHash.guide.js
|
|
14773
|
-
var path = url.replace(/^https?:\/\/[^/]+\/guide-content\//, '').split('/');
|
|
14774
|
-
var guideId = path[0];
|
|
14775
|
-
/* global $A */
|
|
14776
|
-
var resourcePath = $A.get('$Resource.pendoGuide' + base32Encode(toUTF8Array(guideId)));
|
|
14777
|
-
if (_.isString(resourcePath)) {
|
|
14778
|
-
return resourcePath + '/' + path.join('/');
|
|
14779
|
-
}
|
|
14780
|
-
}
|
|
14781
|
-
return url;
|
|
14782
|
-
}
|
|
14783
|
-
function addExtensionToUrl(url, extension) {
|
|
14784
|
-
if (!_.isString(url))
|
|
14785
|
-
return url;
|
|
14786
|
-
var i = _.indexOf(url, '?');
|
|
14787
|
-
if (i < 0) {
|
|
14788
|
-
return url + extension;
|
|
14789
|
-
}
|
|
14790
|
-
else {
|
|
14791
|
-
return url.substring(0, i) + extension + url.substring(i);
|
|
14792
|
-
}
|
|
14793
|
-
}
|
|
14794
|
-
return {
|
|
14795
|
-
load,
|
|
14796
|
-
reset,
|
|
14797
|
-
guideContent,
|
|
14798
|
-
receiveDomStructureJson
|
|
14799
|
-
};
|
|
14800
|
-
})();
|
|
14801
|
-
|
|
14802
14998
|
var AsyncContent = (function () {
|
|
14803
14999
|
function AsyncContent(guide) {
|
|
14804
15000
|
var step = this;
|
|
@@ -14928,7 +15124,7 @@ var GuideRuntime = (function (agentEvents) {
|
|
|
14928
15124
|
}
|
|
14929
15125
|
function teardown() {
|
|
14930
15126
|
EventChannel.off(handleEvent);
|
|
14931
|
-
|
|
15127
|
+
pluginAddGlobalScripts.length = 0;
|
|
14932
15128
|
}
|
|
14933
15129
|
function addGlobalScript(obj) {
|
|
14934
15130
|
obj = autoSubscribe(obj);
|
|
@@ -18391,6 +18587,10 @@ var updateVisitorOptions = function (options = {}) {
|
|
|
18391
18587
|
if (!_.isObject(options)) {
|
|
18392
18588
|
options = {};
|
|
18393
18589
|
}
|
|
18590
|
+
if (isTrackingConsentDenied()) {
|
|
18591
|
+
log.warn('Ignoring identify/updateOptions metadata while identity consent is dnt.');
|
|
18592
|
+
return { options: getMetadata(), hashChanged: false, storeChanged: false };
|
|
18593
|
+
}
|
|
18394
18594
|
if (options.jwt && options.signingKeyName) {
|
|
18395
18595
|
var jwtOptions = JWT.getJwtOptions(options, 'updateOptions');
|
|
18396
18596
|
if (jwtOptions) {
|
|
@@ -23368,7 +23568,7 @@ function autofocusRuntimeScript(step, guide, pendo) {
|
|
|
23368
23568
|
if (pendo._.get(step, 'attributes.isAutoFocus')) {
|
|
23369
23569
|
BuildingBlockGuides.autofocusStep(step, guideContainer);
|
|
23370
23570
|
BuildingBlockGuides.trapFocusStep(step, guideContainer);
|
|
23371
|
-
step.hiddenElements = BuildingBlockGuides.addAriaHiddenOutsideGuide(step.guideElement[0]);
|
|
23571
|
+
step.hiddenElements = (step.hiddenElements || []).concat(BuildingBlockGuides.addAriaHiddenOutsideGuide(step.guideElement[0]));
|
|
23372
23572
|
}
|
|
23373
23573
|
if (isGuideRCType(guide)) {
|
|
23374
23574
|
BuildingBlockGuides.attachRCFocusHandlers();
|
|
@@ -25003,22 +25203,31 @@ function hasLegacyGuides(guides, pendo) {
|
|
|
25003
25203
|
}
|
|
25004
25204
|
const DEFAULT_CSS_ID = '_pendo-default-css_';
|
|
25005
25205
|
const CUSTOM_CSS_ID = '_pendo-css_';
|
|
25206
|
+
// Several SDK instances can share a page. Scoping the <link> ids by api key stops one app's
|
|
25207
|
+
// teardown, or its "no custom css" path, from stripping the stylesheet another app injected.
|
|
25208
|
+
function getCssElementId(baseId) {
|
|
25209
|
+
return pendo$1.apiKey ? baseId + pendo$1.apiKey : baseId;
|
|
25210
|
+
}
|
|
25211
|
+
// Api keys are arbitrary customer-supplied strings, so '#' + id is not a safe selector.
|
|
25212
|
+
function removeGuideCss(baseId) {
|
|
25213
|
+
dom(document.getElementById(getCssElementId(baseId))).remove();
|
|
25214
|
+
}
|
|
25006
25215
|
function loadGuideCss() {
|
|
25007
25216
|
var promises = [];
|
|
25008
25217
|
if (!shouldLoadGlobalCSS()) {
|
|
25009
25218
|
return q.resolve();
|
|
25010
25219
|
}
|
|
25011
25220
|
if (hasLegacyGuides(getActiveGuides(), pendo$1)) {
|
|
25012
|
-
promises.push(loadExternalCss(DEFAULT_CSS_ID, getDefaultCssUrl()));
|
|
25221
|
+
promises.push(loadExternalCss(getCssElementId(DEFAULT_CSS_ID), getDefaultCssUrl()));
|
|
25013
25222
|
}
|
|
25014
25223
|
var guideWidget = pendo$1.guideWidget || {};
|
|
25015
25224
|
var data = guideWidget.data || {};
|
|
25016
25225
|
var customCssUrl = data.guideCssUrl;
|
|
25017
25226
|
if (customCssUrl) {
|
|
25018
|
-
promises.push(loadExternalCss(CUSTOM_CSS_ID, replaceWithContentHost(customCssUrl)));
|
|
25227
|
+
promises.push(loadExternalCss(getCssElementId(CUSTOM_CSS_ID), replaceWithContentHost(customCssUrl)));
|
|
25019
25228
|
}
|
|
25020
25229
|
else {
|
|
25021
|
-
|
|
25230
|
+
removeGuideCss(CUSTOM_CSS_ID);
|
|
25022
25231
|
}
|
|
25023
25232
|
return promises;
|
|
25024
25233
|
}
|
|
@@ -25030,9 +25239,15 @@ var processGuideEventCache = function (options) {
|
|
|
25030
25239
|
var getGuideEventCache = function () {
|
|
25031
25240
|
return guideEvtCache;
|
|
25032
25241
|
};
|
|
25242
|
+
function filterGuideAnalyticsByType(event, next) {
|
|
25243
|
+
if (!hasConsentForGuideEvent(event.type)) {
|
|
25244
|
+
return;
|
|
25245
|
+
}
|
|
25246
|
+
next(event);
|
|
25247
|
+
}
|
|
25033
25248
|
function createGuideEventQueue(options) {
|
|
25034
25249
|
const processSilos = pipeline(filterAnalyticsDisabled, splitSiloOnFieldChange('visitor_id'), splitSiloOnFieldChange('props.source'), filterAccountIdsForSendQueue, compressSilo, errorLogger(), addSiloParams(options), addAccountIdParamIfAdoptPartner, addSourceParamIfPresent, convertSiloToObject);
|
|
25035
|
-
const packageSilos = pipeline(filterBrokenTimestamps, fireEventCaptured, eventSequence, siloReducer(options.cache), _.noop // do not move to the silo queue until manual flush
|
|
25250
|
+
const packageSilos = pipeline(filterBrokenTimestamps, filterGuideAnalyticsByType, fireEventCaptured, eventSequence, siloReducer(options.cache), _.noop // do not move to the silo queue until manual flush
|
|
25036
25251
|
);
|
|
25037
25252
|
return new EventQueue(_.extend({ processSilos, packageSilos }, options));
|
|
25038
25253
|
}
|
|
@@ -25137,6 +25352,12 @@ var initGuides = function (observer) {
|
|
|
25137
25352
|
const { guideIds } = event.data[0];
|
|
25138
25353
|
pruneBadges(guideIds);
|
|
25139
25354
|
});
|
|
25355
|
+
Events['consent:changed'].on(function (event) {
|
|
25356
|
+
var { guides } = event.data[0];
|
|
25357
|
+
if (_.isUndefined(guides))
|
|
25358
|
+
return;
|
|
25359
|
+
guides ? enableGuides() : disableGuides();
|
|
25360
|
+
});
|
|
25140
25361
|
teardownFns.push(onTurbolinksPageLoad(document, function onPageLoad() {
|
|
25141
25362
|
if (pendoDotUrl.get() === reloadGuides.lastUrl) {
|
|
25142
25363
|
// Force a reload if Turbolinks replaces the document body without changing the url
|
|
@@ -25223,8 +25444,8 @@ var initGuides = function (observer) {
|
|
|
25223
25444
|
_.each(teardownFns, function (teardownFn) {
|
|
25224
25445
|
teardownFn();
|
|
25225
25446
|
});
|
|
25226
|
-
|
|
25227
|
-
|
|
25447
|
+
removeGuideCss(DEFAULT_CSS_ID);
|
|
25448
|
+
removeGuideCss(CUSTOM_CSS_ID);
|
|
25228
25449
|
GuideRuntime.teardown();
|
|
25229
25450
|
};
|
|
25230
25451
|
};
|
|
@@ -25257,6 +25478,14 @@ var setGuidesDisabled = function (areDisabled) {
|
|
|
25257
25478
|
originalOptions.disableGuides = areDisabled;
|
|
25258
25479
|
ConfigReader.setLocalConfig(originalOptions);
|
|
25259
25480
|
};
|
|
25481
|
+
function enableGuides() {
|
|
25482
|
+
setGuidesDisabled(false);
|
|
25483
|
+
forceGuideReload();
|
|
25484
|
+
}
|
|
25485
|
+
function disableGuides() {
|
|
25486
|
+
setGuidesDisabled(true);
|
|
25487
|
+
resetPendoUI();
|
|
25488
|
+
}
|
|
25260
25489
|
var setGuidesDelayed = function (areDelayed) {
|
|
25261
25490
|
var originalOptions = ConfigReader.getLocalConfig();
|
|
25262
25491
|
delete originalOptions.delayGuides;
|
|
@@ -25686,6 +25915,8 @@ _.extend(TopFrameRelay.prototype, {
|
|
|
25686
25915
|
if (!message.data)
|
|
25687
25916
|
return;
|
|
25688
25917
|
if (message.data.action === 'pendo::connect') {
|
|
25918
|
+
if (!isSameWindowTree(message, this._window))
|
|
25919
|
+
return;
|
|
25689
25920
|
this._impl.frameConnect(message);
|
|
25690
25921
|
}
|
|
25691
25922
|
if (message.data.action === REMOVE_FRAME_ACTION) {
|
|
@@ -25781,6 +26012,38 @@ var ImplCommon = {
|
|
|
25781
26012
|
function isPortValid(port) {
|
|
25782
26013
|
return port && _.all(['addEventListener', 'start'], function (cmd) { return _.isFunction(port[cmd]); });
|
|
25783
26014
|
}
|
|
26015
|
+
// A connect is only ever posted to window.top by a frame below us, so a sender we
|
|
26016
|
+
// cannot find in our own frame tree is not one of our frames no matter what it
|
|
26017
|
+
// claims. window.open gives an attacker a handle to post through, and neither
|
|
26018
|
+
// X-Frame-Options nor frame-ancestors covers that - they only govern embedding
|
|
26019
|
+
// (APP-168708). frames and its indexed children are readable across origins, so
|
|
26020
|
+
// this holds where an origin check can't: no ancestorOrigins needed, and sandboxed
|
|
26021
|
+
// frames (origin "null") are still found.
|
|
26022
|
+
function isFrameBelowUs(source, window) {
|
|
26023
|
+
if (!source)
|
|
26024
|
+
return false;
|
|
26025
|
+
if (source === window)
|
|
26026
|
+
return true;
|
|
26027
|
+
var pending = [window];
|
|
26028
|
+
while (pending.length) {
|
|
26029
|
+
var children = pending.shift().frames;
|
|
26030
|
+
var count = children ? children.length : 0;
|
|
26031
|
+
for (var i = 0; i < count; i++) {
|
|
26032
|
+
if (children[i] === source)
|
|
26033
|
+
return true;
|
|
26034
|
+
pending.push(children[i]);
|
|
26035
|
+
}
|
|
26036
|
+
}
|
|
26037
|
+
return false;
|
|
26038
|
+
}
|
|
26039
|
+
function isSameWindowTree(message, window) {
|
|
26040
|
+
try {
|
|
26041
|
+
return isFrameBelowUs(message.source, window);
|
|
26042
|
+
}
|
|
26043
|
+
catch (e) {
|
|
26044
|
+
return false;
|
|
26045
|
+
}
|
|
26046
|
+
}
|
|
25784
26047
|
function MessageChannelImpl(window, singletonMessageHandler) {
|
|
25785
26048
|
this._window = window;
|
|
25786
26049
|
this._ports = {};
|
|
@@ -27017,11 +27280,24 @@ const Keys = (function () {
|
|
|
27017
27280
|
})();
|
|
27018
27281
|
|
|
27019
27282
|
const pendoLocalStorage = agentStorage.getLocal();
|
|
27283
|
+
const pendoSessionStorage$1 = agentStorage.getSession();
|
|
27284
|
+
const DESIGNER_LAUNCH_RECEIPT = 'pendo-designer-launch-receipt';
|
|
27020
27285
|
var DesignerV2 = (function () {
|
|
27021
27286
|
// Always use the production server for CNAME customers
|
|
27022
27287
|
var host = DESIGNER_SERVER || SERVER;
|
|
27023
27288
|
var bucket = 'in-app-designer';
|
|
27024
27289
|
var target = 'latest';
|
|
27290
|
+
/**
|
|
27291
|
+
* The designerId of a designer session launched from this tab. Read back after a
|
|
27292
|
+
* page reload to tell a relaunch of that session apart from a URL token planted
|
|
27293
|
+
* by another origin.
|
|
27294
|
+
*
|
|
27295
|
+
* @name pendo-designer-launch-receipt
|
|
27296
|
+
* @category Cookies/sessionStorage
|
|
27297
|
+
* @access public
|
|
27298
|
+
* @label DESIGNER_LAUNCH_RECEIPT
|
|
27299
|
+
*/
|
|
27300
|
+
agentStorage.registry.addSession(DESIGNER_LAUNCH_RECEIPT);
|
|
27025
27301
|
Events.leaderChanged.on(function () {
|
|
27026
27302
|
// check to make sure that we aren't trying to launch nav mode from a designer frame
|
|
27027
27303
|
var inDesignerOrPreview = /pendo-/.test(window.name) || isInDesignerPreviewMode();
|
|
@@ -27146,13 +27422,20 @@ var DesignerV2 = (function () {
|
|
|
27146
27422
|
return false;
|
|
27147
27423
|
}
|
|
27148
27424
|
}
|
|
27425
|
+
function writeLaunchReceipt(designerId) {
|
|
27426
|
+
if (!designerId)
|
|
27427
|
+
return;
|
|
27428
|
+
pendoSessionStorage$1.setItem(DESIGNER_LAUNCH_RECEIPT, designerId);
|
|
27429
|
+
}
|
|
27430
|
+
function hasLaunchReceipt(designerId) {
|
|
27431
|
+
if (!designerId)
|
|
27432
|
+
return false;
|
|
27433
|
+
return pendoSessionStorage$1.getItem(DESIGNER_LAUNCH_RECEIPT) === designerId;
|
|
27434
|
+
}
|
|
27149
27435
|
function launchOnToken(url) {
|
|
27150
27436
|
if (isDesignerFrame(window.name)) {
|
|
27151
27437
|
return;
|
|
27152
27438
|
}
|
|
27153
|
-
if (isPendoDomain(window.location.origin) && !hasSameOriginOpener()) {
|
|
27154
|
-
return;
|
|
27155
|
-
}
|
|
27156
27439
|
var tokenParamTest = /pendo-designer=([A-Za-z0-9-]+)/;
|
|
27157
27440
|
if (!tokenParamTest.test(url))
|
|
27158
27441
|
return;
|
|
@@ -27160,6 +27443,9 @@ var DesignerV2 = (function () {
|
|
|
27160
27443
|
if (!tokenMatch)
|
|
27161
27444
|
return;
|
|
27162
27445
|
var parsedToken = parsePendoToken(tokenMatch[1]);
|
|
27446
|
+
if (isPendoDomain(window.location.origin) && !hasSameOriginOpener() && !hasLaunchReceipt(parsedToken.designerId)) {
|
|
27447
|
+
return;
|
|
27448
|
+
}
|
|
27163
27449
|
var validAssetHost = getValidDesignerHost(parsedToken);
|
|
27164
27450
|
var validTarget = getValidTarget(parsedToken);
|
|
27165
27451
|
// clear stored designer state if we're launching from a token
|
|
@@ -27211,6 +27497,7 @@ var DesignerV2 = (function () {
|
|
|
27211
27497
|
if (!options) {
|
|
27212
27498
|
options = {};
|
|
27213
27499
|
}
|
|
27500
|
+
writeLaunchReceipt(options.designerId);
|
|
27214
27501
|
var validAssetHost = getValidDesignerHost(options);
|
|
27215
27502
|
var validBucket = getValidGCSBucket(options);
|
|
27216
27503
|
var validTarget = getValidTarget(options);
|
|
@@ -28960,7 +29247,10 @@ const PluginAPI = {
|
|
|
28960
29247
|
agentStorage,
|
|
28961
29248
|
analytics: {
|
|
28962
29249
|
ptm: () => eventQueue,
|
|
28963
|
-
collectEvent
|
|
29250
|
+
collectEvent,
|
|
29251
|
+
hasConsentForAnalyticsEvent,
|
|
29252
|
+
getTextCollectionMode,
|
|
29253
|
+
isReplayConsentDenied
|
|
28964
29254
|
},
|
|
28965
29255
|
attachEvent,
|
|
28966
29256
|
attachEventInternal,
|
|
@@ -29163,14 +29453,27 @@ function initIdentityEvents() {
|
|
|
29163
29453
|
}
|
|
29164
29454
|
}
|
|
29165
29455
|
};
|
|
29456
|
+
const requestReidentification = function () {
|
|
29457
|
+
if (!isTrackingConsentDenied() && isVisitorDNT()) {
|
|
29458
|
+
Events.identifyRequested.trigger();
|
|
29459
|
+
}
|
|
29460
|
+
};
|
|
29461
|
+
const handleConsentChange = function (event) {
|
|
29462
|
+
var { identity } = event.data[0];
|
|
29463
|
+
if (_.isUndefined(identity))
|
|
29464
|
+
return;
|
|
29465
|
+
identity ? requestReidentification() : clearSession({ doNotTrack: true });
|
|
29466
|
+
};
|
|
29166
29467
|
Events.identify.on(collectIdentifyEvent);
|
|
29167
29468
|
Events.metadata.on(collectMetadataEvent);
|
|
29168
29469
|
Events.identify.on(handleIdentifyEvent);
|
|
29470
|
+
Events['consent:changed'].on(handleConsentChange);
|
|
29169
29471
|
initialized = true;
|
|
29170
29472
|
return function () {
|
|
29171
29473
|
Events.identify.off(collectIdentifyEvent);
|
|
29172
29474
|
Events.metadata.off(collectMetadataEvent);
|
|
29173
29475
|
Events.identify.off(handleIdentifyEvent);
|
|
29476
|
+
Events['consent:changed'].off(handleConsentChange);
|
|
29174
29477
|
initialized = false;
|
|
29175
29478
|
};
|
|
29176
29479
|
}
|
|
@@ -29524,7 +29827,7 @@ function registerEventHandlers({ events = [] }) {
|
|
|
29524
29827
|
function applyDoNotTrackConfigOverrides(options) {
|
|
29525
29828
|
options.excludeNonGuideAnalytics = true;
|
|
29526
29829
|
options.disableCookies = true;
|
|
29527
|
-
options.visitor = { id:
|
|
29830
|
+
options.visitor = { id: COOKIELESS_VISITOR_ID };
|
|
29528
29831
|
delete options.account;
|
|
29529
29832
|
delete options.parentAccount;
|
|
29530
29833
|
}
|
|
@@ -30901,6 +31204,28 @@ logPublic.setActiveContexts = setActiveContexts;
|
|
|
30901
31204
|
logPublic.showLogHistory = showLogHistory;
|
|
30902
31205
|
logPublic.getLoggedContexts = getLoggedContexts;
|
|
30903
31206
|
|
|
31207
|
+
/**
|
|
31208
|
+
* Enforces the cookie consent policy a visitor has agreed to. The policy is defined by the subscription's cookie consent configuration,
|
|
31209
|
+
* and is used to determine whether each product the web SDK may run.
|
|
31210
|
+
*
|
|
31211
|
+
* @access public
|
|
31212
|
+
* @category Core
|
|
31213
|
+
* @param {string | string[]} tiers tier or tiers the visitor consented to
|
|
31214
|
+
* @returns {object} map of product to its resolved value -- `guides` and `identity` are
|
|
31215
|
+
* booleans (identity disabled means DNT), `textCollection` is `none`/`whitelist`/`all`,
|
|
31216
|
+
* `analytics` and `guideAnalytics` are each an array of allowed event types
|
|
31217
|
+
* @example
|
|
31218
|
+
* pendo.updateCookieConsent('strictlyNecessary');
|
|
31219
|
+
* @example
|
|
31220
|
+
* pendo.updateCookieConsent(['strictlyNecessary', 'functional']);
|
|
31221
|
+
*/
|
|
31222
|
+
function updateCookieConsent(tiers) {
|
|
31223
|
+
const enabledProducts = applyConsentTiers(tiers);
|
|
31224
|
+
Events['consent:changed'].trigger(enabledProducts);
|
|
31225
|
+
log.debug('Cookie consent updated for ' + JSON.stringify(tiers) + '.');
|
|
31226
|
+
return enabledProducts;
|
|
31227
|
+
}
|
|
31228
|
+
|
|
30904
31229
|
/**
|
|
30905
31230
|
* Shared helper for event collection
|
|
30906
31231
|
*/
|
|
@@ -31973,6 +32298,7 @@ function exportPublicApi(pendo) {
|
|
|
31973
32298
|
// cache.js
|
|
31974
32299
|
pendo.flushNow = exportPendoCoreOnly(flushNow);
|
|
31975
32300
|
pendo.teardown = makeSafe(teardown);
|
|
32301
|
+
pendo.updateCookieConsent = makeSafe(updateCookieConsent);
|
|
31976
32302
|
// guides.js
|
|
31977
32303
|
pendo.initGuides = exportPendoCoreOnly(initGuides);
|
|
31978
32304
|
pendo.loadGuides = exportPendoCoreOnly(loadGuides);
|
|
@@ -34512,12 +34838,15 @@ var FramesModule = (function () {
|
|
|
34512
34838
|
receiveLastGuideStepSeen(context, payload) {
|
|
34513
34839
|
if (payload.frameId === context.state.frameId)
|
|
34514
34840
|
return;
|
|
34515
|
-
// The extension and native installs will never have the same visitor id
|
|
34841
|
+
// The extension and native installs will never have the same visitor id,
|
|
34842
|
+
// and neither will frames from different applications (apiKeys).
|
|
34516
34843
|
// However, all frames need to keep an accurate record of what has been viewed, to ensure
|
|
34517
|
-
// that
|
|
34844
|
+
// that one agent's frames do not attempt to re-display another agent's guides
|
|
34518
34845
|
var originatingFrame = context.state.frames[payload.frameId];
|
|
34519
34846
|
var originatingInstallType = _.get(originatingFrame, 'agentInstallType');
|
|
34520
|
-
|
|
34847
|
+
var originatingApiKey = _.get(originatingFrame, 'apiKey');
|
|
34848
|
+
if ((originatingInstallType && originatingInstallType !== context.state.agentInstallType) ||
|
|
34849
|
+
(originatingApiKey && originatingApiKey !== pendo$1.apiKey)) {
|
|
34521
34850
|
delete payload.lastGuideStepSeen.visitorId;
|
|
34522
34851
|
}
|
|
34523
34852
|
context.dispatch('guideState/updateLastGuideStepSeen', payload.lastGuideStepSeen, { root: true });
|
|
@@ -42313,9 +42642,11 @@ function TextCapture() {
|
|
|
42313
42642
|
}
|
|
42314
42643
|
function isEnabled() {
|
|
42315
42644
|
// this preserves the ability for a user to disable text collection after initialization
|
|
42316
|
-
return !pendoGlobal.excludeAllText;
|
|
42645
|
+
return !pendoGlobal.excludeAllText && pluginAPI.analytics.getTextCollectionMode() === 'all';
|
|
42317
42646
|
}
|
|
42318
42647
|
function isTextCapturable(text) {
|
|
42648
|
+
if (pluginAPI.analytics.getTextCollectionMode() === 'none')
|
|
42649
|
+
return false;
|
|
42319
42650
|
var trimmedText = pluginAPI.util.trim.call(text);
|
|
42320
42651
|
// pass the text as an array so that our toPath override doesn't try to split it on `.`
|
|
42321
42652
|
return isEnabled() || pendoGlobal._.has(whitelist, [trimmedText]) || pendoGlobal._.has(whitelist, [text]);
|
|
@@ -42852,6 +43183,124 @@ function substitutionIcon(color, size) {
|
|
|
42852
43183
|
13.7239 15.5 14V17Z" fill="${color}"></path> </g></svg></div>`);
|
|
42853
43184
|
}
|
|
42854
43185
|
|
|
43186
|
+
const AUTO_ADVANCE_DELAY_MS = 250;
|
|
43187
|
+
function isAutoAdvanceRequested(step, _) {
|
|
43188
|
+
return _.get(step, 'attributes.autoAdvance') === true;
|
|
43189
|
+
}
|
|
43190
|
+
// Survey-authored DOM carries no question-type classes, so eligibility keys off
|
|
43191
|
+
// answer-control shape: every required poll id must be radio-backed, which also
|
|
43192
|
+
// refuses a future non-radio type by default.
|
|
43193
|
+
function isStepEligibleInDom(step, _, requiredPollIds) {
|
|
43194
|
+
const guideElement = step && step.guideElement;
|
|
43195
|
+
if (!guideElement)
|
|
43196
|
+
return false;
|
|
43197
|
+
if (guideElement.find('textarea, input:text, input:checkbox, select').length)
|
|
43198
|
+
return false;
|
|
43199
|
+
const pollIds = _.uniq(_.map(guideElement.find('[data-pendo-poll-id]'), (element) => element.getAttribute('data-pendo-poll-id')));
|
|
43200
|
+
if (!pollIds.length)
|
|
43201
|
+
return false;
|
|
43202
|
+
return _.all(pollIds, (pollId) => _.contains(requiredPollIds, pollId) &&
|
|
43203
|
+
guideElement.find(`input:radio[data-pendo-poll-id="${pollId}"]`).length > 0);
|
|
43204
|
+
}
|
|
43205
|
+
function hideSubmitButtons(step, _, submitButtons) {
|
|
43206
|
+
const hiddenButtons = [];
|
|
43207
|
+
_.each(submitButtons, (button) => {
|
|
43208
|
+
const element = step.guideElement.find(`#${button.props.id}`)[0];
|
|
43209
|
+
if (!element)
|
|
43210
|
+
return;
|
|
43211
|
+
// display, not visibility: a reserved-but-invisible box leaves an empty gap
|
|
43212
|
+
// at the foot of the step. Authored styles reach the button inline
|
|
43213
|
+
// (build.js:151), so the original value is kept rather than blanked.
|
|
43214
|
+
hiddenButtons.push({ element, display: element.style.display });
|
|
43215
|
+
element.style.display = 'none';
|
|
43216
|
+
});
|
|
43217
|
+
return function restoreSubmitButtons() {
|
|
43218
|
+
_.each(hiddenButtons, (hiddenButton) => {
|
|
43219
|
+
hiddenButton.element.style.display = hiddenButton.display;
|
|
43220
|
+
});
|
|
43221
|
+
};
|
|
43222
|
+
}
|
|
43223
|
+
// The container's pixel height is measured during render (layout.js) before custom
|
|
43224
|
+
// scripts run, so collapsing a row afterwards leaves the box stale and the step keeps
|
|
43225
|
+
// the empty space. Same reflow pair metadata-substitution uses, scoped to this step so
|
|
43226
|
+
// a second guide on screen is left alone.
|
|
43227
|
+
function reflowGuide(step, _, pendo) {
|
|
43228
|
+
const recalculateGuideHeight = _.get(pendo, 'BuildingBlocks.BuildingBlockGuides.recalculateGuideHeight');
|
|
43229
|
+
try {
|
|
43230
|
+
if (_.isFunction(pendo.flexElement)) {
|
|
43231
|
+
pendo.flexElement(step.guideElement.find(guideMarkdownUtil.containerSelector));
|
|
43232
|
+
}
|
|
43233
|
+
if (_.isFunction(recalculateGuideHeight)) {
|
|
43234
|
+
recalculateGuideHeight(`pendo-g-${step.id}`, step.guideElement);
|
|
43235
|
+
}
|
|
43236
|
+
}
|
|
43237
|
+
catch (error) {
|
|
43238
|
+
pendo.log('[auto-advance] guide reflow failed, the step may keep an empty row', error && error.message);
|
|
43239
|
+
}
|
|
43240
|
+
}
|
|
43241
|
+
function createInertController() {
|
|
43242
|
+
return { isActive: false, notifyAnswered() { }, cancel() { } };
|
|
43243
|
+
}
|
|
43244
|
+
function createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds) {
|
|
43245
|
+
const _ = pendo._;
|
|
43246
|
+
const inert = createInertController();
|
|
43247
|
+
if (!isAutoAdvanceRequested(step, _))
|
|
43248
|
+
return inert;
|
|
43249
|
+
// Only reachable for API-authored guides; choosing between two
|
|
43250
|
+
// submitPollAndGoToStep targets would be a guess.
|
|
43251
|
+
if (_.size(submitButtons) !== 1) {
|
|
43252
|
+
pendo.log(`[auto-advance] not arming: step ${step.id} needs exactly one submit button, found ${_.size(submitButtons)}`);
|
|
43253
|
+
return inert;
|
|
43254
|
+
}
|
|
43255
|
+
if (!isStepEligibleInDom(step, _, requiredPollIds)) {
|
|
43256
|
+
pendo.log(`[auto-advance] not arming: rendered step ${step.id} failed the eligibility re-check`);
|
|
43257
|
+
return inert;
|
|
43258
|
+
}
|
|
43259
|
+
const submitButton = submitButtons[0];
|
|
43260
|
+
// A click binds every action on the node and dispatches one event each
|
|
43261
|
+
// (build.js:205-207); we dispatch one, so a second action would be dropped.
|
|
43262
|
+
if (_.size(submitButton.actions) !== 1) {
|
|
43263
|
+
pendo.log(`[auto-advance] not arming: step ${step.id} submit button carries ${_.size(submitButton.actions)} actions, expected one`);
|
|
43264
|
+
return inert;
|
|
43265
|
+
}
|
|
43266
|
+
const submitAction = _.find(submitButton.actions, (action) => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep');
|
|
43267
|
+
if (!submitAction)
|
|
43268
|
+
return inert;
|
|
43269
|
+
let hasFired = false;
|
|
43270
|
+
let restoreSubmitButtons = _.noop;
|
|
43271
|
+
function fire() {
|
|
43272
|
+
if (hasFired)
|
|
43273
|
+
return;
|
|
43274
|
+
hasFired = true;
|
|
43275
|
+
try {
|
|
43276
|
+
// Shape-identical to what bindActionToNode emits for a click, so
|
|
43277
|
+
// beforeAdvance consumers intercept an auto-advance the same way.
|
|
43278
|
+
step.eventRouter.eventable.trigger('pendoEvent', {
|
|
43279
|
+
action: submitAction.action,
|
|
43280
|
+
params: submitAction.parameters,
|
|
43281
|
+
step,
|
|
43282
|
+
ignore: !!submitAction.ignore,
|
|
43283
|
+
srcElement: step.guideElement.find(`#${submitButton.props.id}`)[0]
|
|
43284
|
+
});
|
|
43285
|
+
}
|
|
43286
|
+
catch (error) {
|
|
43287
|
+
restoreSubmitButtons();
|
|
43288
|
+
reflowGuide(step, _, pendo);
|
|
43289
|
+
pendo.log('[auto-advance] submit dispatch failed, restored the submit button', error && error.message);
|
|
43290
|
+
}
|
|
43291
|
+
}
|
|
43292
|
+
const debouncedFire = _.debounce(fire, AUTO_ADVANCE_DELAY_MS);
|
|
43293
|
+
// Last, so nothing between here and the return can throw and leave the step
|
|
43294
|
+
// with a hidden button and no controller to un-hide it.
|
|
43295
|
+
restoreSubmitButtons = hideSubmitButtons(step, _, submitButtons);
|
|
43296
|
+
reflowGuide(step, _, pendo);
|
|
43297
|
+
return {
|
|
43298
|
+
isActive: true,
|
|
43299
|
+
notifyAnswered: debouncedFire,
|
|
43300
|
+
cancel: debouncedFire.cancel
|
|
43301
|
+
};
|
|
43302
|
+
}
|
|
43303
|
+
|
|
42855
43304
|
const requiredElement = '<span class="_pendo-required-indicator" style="color:red; font-style:italic;" title="Question is required"> *</span>';
|
|
42856
43305
|
const requiredSyntax = '{required/}';
|
|
42857
43306
|
const RequiredQuestions = {
|
|
@@ -42861,6 +43310,7 @@ const RequiredQuestions = {
|
|
|
42861
43310
|
let requiredPollIds = [];
|
|
42862
43311
|
let submitButtons = (_a = guideMarkdownUtil.lookupGuideButtons(step.domJson)) === null || _a === void 0 ? void 0 : _a.filter(button => button.actions.find(action => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep'));
|
|
42863
43312
|
const requiredQuestions = processRequiredQuestions();
|
|
43313
|
+
let autoAdvance = createInertController();
|
|
42864
43314
|
if (requiredQuestions) {
|
|
42865
43315
|
pendo._.forEach(requiredQuestions, (question) => {
|
|
42866
43316
|
if (question.classList.contains('_pendo-open-text-poll-question')) {
|
|
@@ -42876,6 +43326,17 @@ const RequiredQuestions = {
|
|
|
42876
43326
|
}
|
|
42877
43327
|
});
|
|
42878
43328
|
}
|
|
43329
|
+
// Armed last, after the listeners above: runCustomScripts abandons the rest of
|
|
43330
|
+
// this script on a throw, and the submit button is already disabled by now.
|
|
43331
|
+
if (isAutoAdvanceRequested(step, pendo._)) {
|
|
43332
|
+
this.on('beforeUnmount', () => autoAdvance.cancel());
|
|
43333
|
+
try {
|
|
43334
|
+
autoAdvance = createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds);
|
|
43335
|
+
}
|
|
43336
|
+
catch (error) {
|
|
43337
|
+
pendo.log('[auto-advance] arming failed, leaving the step on manual submit', error && error.message);
|
|
43338
|
+
}
|
|
43339
|
+
}
|
|
42879
43340
|
function getEligibleQuestions() {
|
|
42880
43341
|
const pollQuestions = pendo._.toArray(step.guideElement.find(`[class*=-poll-question]:contains(${requiredSyntax})`));
|
|
42881
43342
|
const surveyMatches = pendo._.toArray(step.guideElement.find(`[data-pendo-poll-id]:contains(${requiredSyntax})`));
|
|
@@ -42952,9 +43413,11 @@ const RequiredQuestions = {
|
|
|
42952
43413
|
}
|
|
42953
43414
|
if (allRequiredComplete) {
|
|
42954
43415
|
enableEligibleButtons(submitButtons);
|
|
43416
|
+
autoAdvance.notifyAnswered();
|
|
42955
43417
|
}
|
|
42956
43418
|
else {
|
|
42957
43419
|
disableEligibleButtons(submitButtons);
|
|
43420
|
+
autoAdvance.cancel();
|
|
42958
43421
|
}
|
|
42959
43422
|
}
|
|
42960
43423
|
function disableEligibleButtons(buttons) {
|
|
@@ -51405,7 +51868,8 @@ class SessionRecorder {
|
|
|
51405
51868
|
this.api.attachEvent(this.api.Events, 'eventCaptured', bind(this.addRecordingId, this)),
|
|
51406
51869
|
this.api.attachEvent(this.api.Events, 'transmit:locked', bind(this.stop, this)),
|
|
51407
51870
|
this.api.attachEvent(this.api.Events, 'transmit:unlocked', bind(this._start, this)),
|
|
51408
|
-
this.api.attachEvent(this.api.Events, 'tabIdChanged:self', bind(this.handleTabIdChange, this))
|
|
51871
|
+
this.api.attachEvent(this.api.Events, 'tabIdChanged:self', bind(this.handleTabIdChange, this)),
|
|
51872
|
+
this.api.attachEvent(this.api.Events, 'consent:changed', bind(this.handleConsentChange, this))
|
|
51409
51873
|
];
|
|
51410
51874
|
// APP-153357 Zone.js could bypass filterCrossWindowMessages (don't use attachEvent)
|
|
51411
51875
|
const channel = this.api.frames.getChannel();
|
|
@@ -51516,6 +51980,20 @@ class SessionRecorder {
|
|
|
51516
51980
|
return this.checkVisitorEligibility();
|
|
51517
51981
|
}
|
|
51518
51982
|
}
|
|
51983
|
+
handleConsentChange(event) {
|
|
51984
|
+
var { identity, replay } = event.data[0];
|
|
51985
|
+
if (this.pendo._.isUndefined(identity) && this.pendo._.isUndefined(replay))
|
|
51986
|
+
return;
|
|
51987
|
+
if (this.api.analytics.isReplayConsentDenied()) {
|
|
51988
|
+
if (this.isRecording()) {
|
|
51989
|
+
this.logStopReason('CONSENT_DENIED');
|
|
51990
|
+
this.stop();
|
|
51991
|
+
}
|
|
51992
|
+
}
|
|
51993
|
+
else if (!this.isRecording()) {
|
|
51994
|
+
this.start();
|
|
51995
|
+
}
|
|
51996
|
+
}
|
|
51519
51997
|
checkVisitorEligibility(continuationCallback) {
|
|
51520
51998
|
if (!this.isRecording() && this.pendo._.isNull(this.visitorId) && this.pendo._.isNull(this.accountId))
|
|
51521
51999
|
return;
|
|
@@ -51598,6 +52076,8 @@ class SessionRecorder {
|
|
|
51598
52076
|
* pendo.recording.start()
|
|
51599
52077
|
*/
|
|
51600
52078
|
start() {
|
|
52079
|
+
if (this.api.analytics.isReplayConsentDenied())
|
|
52080
|
+
return;
|
|
51601
52081
|
this._restartPtm = this.api.analytics.ptm().pause();
|
|
51602
52082
|
this.allowStart = true;
|
|
51603
52083
|
this.visitorId = this.pendo.get_visitor_id();
|
|
@@ -51655,6 +52135,11 @@ class SessionRecorder {
|
|
|
51655
52135
|
});
|
|
51656
52136
|
}
|
|
51657
52137
|
_startRecordingForVisitor(visitorConfig) {
|
|
52138
|
+
if (this.api.analytics.isReplayConsentDenied()) {
|
|
52139
|
+
this.restartPtm();
|
|
52140
|
+
this.logStopReason('CONSENT_DENIED');
|
|
52141
|
+
return;
|
|
52142
|
+
}
|
|
51658
52143
|
this.api.Events.trigger('recording:unpaused');
|
|
51659
52144
|
this.transport.start(this.config);
|
|
51660
52145
|
const disableFallback = this.pendo._.get(this.config, 'disableFallback', true);
|
|
@@ -56009,6 +56494,8 @@ class PredictApiService {
|
|
|
56009
56494
|
|
|
56010
56495
|
const PREDICT_STEP_REGEX = /\$\$predict\$\$/;
|
|
56011
56496
|
const DRAG_THRESHOLD = 5;
|
|
56497
|
+
const FLOATING_MODAL_OFFSET = '20px';
|
|
56498
|
+
const DEFAULT_INITIAL_POSITION = 'TOP_RIGHT';
|
|
56012
56499
|
const STYLE_ID = 'pendo-predict-plugin-styles';
|
|
56013
56500
|
const GUIDE_BUTTON_IDENTIFIER = 'button:contains("token")';
|
|
56014
56501
|
const pluginVersion = '1.0.1';
|
|
@@ -56070,7 +56557,7 @@ const injectStyles = (pendoContainerId, log = () => { }) => {
|
|
|
56070
56557
|
}
|
|
56071
56558
|
.frame-explanation {
|
|
56072
56559
|
aspect-ratio: 16/9; overflow: hidden; min-height: 300px;
|
|
56073
|
-
width:
|
|
56560
|
+
width: 554px; height: 100vh; background: transparent;
|
|
56074
56561
|
position: fixed; bottom: 0; right: 0; z-index: 1; border: none;
|
|
56075
56562
|
visibility: hidden; opacity: 0; pointer-events: none;
|
|
56076
56563
|
transition: opacity 0.1s ease-out, visibility 0s linear 0.1s;
|
|
@@ -56228,7 +56715,9 @@ const setupDragAndDrop = (handle, container, frameTargetOrigin) => {
|
|
|
56228
56715
|
initialLeft = rect.left;
|
|
56229
56716
|
initialTop = rect.top;
|
|
56230
56717
|
container.style.left = `${initialLeft}px`;
|
|
56718
|
+
container.style.top = `${initialTop}px`;
|
|
56231
56719
|
container.style.right = 'auto';
|
|
56720
|
+
container.style.bottom = 'auto';
|
|
56232
56721
|
startX = e.clientX;
|
|
56233
56722
|
startY = e.clientY;
|
|
56234
56723
|
document.addEventListener('mousemove', onMouseMove);
|
|
@@ -56301,6 +56790,19 @@ const createExplanationIframe = ({ analysisId, recordId, configuration, predictB
|
|
|
56301
56790
|
src: buildFrameSrc(`${base}?${params}`, wrapperBase)
|
|
56302
56791
|
}, document.body);
|
|
56303
56792
|
};
|
|
56793
|
+
// The floating modal defaults to the top-right corner (see injected styles).
|
|
56794
|
+
// `initialPosition` overrides that by anchoring the container to the requested
|
|
56795
|
+
// corner; the drag logic later switches to explicit left/top on first drag.
|
|
56796
|
+
const applyInitialPosition = (container, initialPosition = DEFAULT_INITIAL_POSITION) => {
|
|
56797
|
+
var _a;
|
|
56798
|
+
const corners = {
|
|
56799
|
+
TOP_LEFT: { top: FLOATING_MODAL_OFFSET, left: FLOATING_MODAL_OFFSET, bottom: 'auto', right: 'auto' },
|
|
56800
|
+
TOP_RIGHT: { top: FLOATING_MODAL_OFFSET, right: FLOATING_MODAL_OFFSET, bottom: 'auto', left: 'auto' },
|
|
56801
|
+
BOTTOM_LEFT: { bottom: FLOATING_MODAL_OFFSET, left: FLOATING_MODAL_OFFSET, top: 'auto', right: 'auto' },
|
|
56802
|
+
BOTTOM_RIGHT: { bottom: FLOATING_MODAL_OFFSET, right: FLOATING_MODAL_OFFSET, top: 'auto', left: 'auto' }
|
|
56803
|
+
};
|
|
56804
|
+
Object.assign(container.style, (_a = corners[initialPosition]) !== null && _a !== void 0 ? _a : corners[DEFAULT_INITIAL_POSITION]);
|
|
56805
|
+
};
|
|
56304
56806
|
// ----- Floating Modal Creation -----
|
|
56305
56807
|
const createFloatingModal = ({ recordId, configuration, predictBaseUrl, wrapperBase = '' }) => {
|
|
56306
56808
|
var _a, _b;
|
|
@@ -56315,6 +56817,7 @@ const createFloatingModal = ({ recordId, configuration, predictBaseUrl, wrapperB
|
|
|
56315
56817
|
});
|
|
56316
56818
|
}
|
|
56317
56819
|
const container = createElement('div', { id: 'floatingModalContainer', className: 'floating-modal-container' }, document.body);
|
|
56820
|
+
applyInitialPosition(container, configuration.initialPosition);
|
|
56318
56821
|
const dragHandle = createElement('div', {
|
|
56319
56822
|
className: 'floating-modal-drag-handle',
|
|
56320
56823
|
innerHTML: `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -56455,7 +56958,7 @@ const fetchWidgetConfiguration = (_a) => __awaiter(void 0, void 0, void 0, funct
|
|
|
56455
56958
|
if (!widgetSettings) {
|
|
56456
56959
|
return inlineConfiguration;
|
|
56457
56960
|
}
|
|
56458
|
-
return Object.assign(Object.assign({}, inlineConfiguration), { analysisIds: mapAnalysisIds(widgetSettings.analysesHierarchy), recordRegexPattern: widgetSettings.recordRegexPattern, recordRegexName: widgetSettings.recordRegexKeyword });
|
|
56961
|
+
return Object.assign(Object.assign({}, inlineConfiguration), { analysisIds: mapAnalysisIds(widgetSettings.analysesHierarchy), recordRegexPattern: widgetSettings.recordRegexPattern, recordRegexName: widgetSettings.recordRegexKeyword, initialPosition: widgetSettings.initialPosition });
|
|
56459
56962
|
});
|
|
56460
56963
|
const predictGuidesScript = ({ step, pendo, cleanupArray, cleanup, log, designerServer }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
56461
56964
|
var _c;
|