@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.
@@ -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.339.1_';
4050
- let PACKAGE_VERSION = '2.339.1';
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$2 = createStorageShim(function () { return window.sessionStorage; });
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
- // This config setting really describes any persistence at all.
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$2);
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$1 = agentStorage.getSession();
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$1);
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
- get_visitor_id({ generate: true });
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
- return ajax.get(url).then(function (response) {
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 "' + url + '"');
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
- _.each(pluginAddGlobalScripts, GuideRuntime.removeGlobalScript);
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
- dom('#' + CUSTOM_CSS_ID).remove();
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
- dom('#' + DEFAULT_CSS_ID).remove();
25227
- dom('#' + CUSTOM_CSS_ID).remove();
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: '__cookieless_visitor__' };
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 native frames do not attempt to re-display extension guides, and vice versa
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
- if (originatingInstallType && originatingInstallType !== context.state.agentInstallType) {
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: 100vw; height: 100vh; background: transparent;
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;