@pendo/agent 2.342.0 → 2.344.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.
@@ -1,5 +1,26 @@
1
1
  function loadAgent(PendoConfig) {
2
2
 
3
+ let policy;
4
+ const policyOptions = {
5
+ createScriptURL: (str) => str,
6
+ createHTML: (str) => str
7
+ };
8
+ function getPolicy(pendo) {
9
+ if (!policy) {
10
+ if (pendo.trustedTypesPolicy) {
11
+ policy = pendo.trustedTypesPolicy;
12
+ }
13
+ else if (window.trustedTypes && typeof window.trustedTypes.createPolicy === 'function') {
14
+ policy = window.trustedTypes.createPolicy('pendo', policyOptions);
15
+ }
16
+ else {
17
+ policy = policyOptions;
18
+ }
19
+ pendo.trustedTypesPolicy = policy;
20
+ }
21
+ return policy;
22
+ }
23
+
3
24
  var b64 = (function() {
4
25
 
5
26
  var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split('');
@@ -409,93 +430,52 @@ var sha1$1 = {exports: {}};
409
430
  var sha1Exports = sha1$1.exports;
410
431
  var sha1 = /*@__PURE__*/getDefaultExportFromCjs(sha1Exports);
411
432
 
412
- let policy;
413
- const policyOptions = {
414
- createScriptURL: (str) => str,
415
- createHTML: (str) => str
416
- };
417
- function getPolicy(pendo) {
418
- if (!policy) {
419
- if (pendo.trustedTypesPolicy) {
420
- policy = pendo.trustedTypesPolicy;
421
- }
422
- else if (window.trustedTypes && typeof window.trustedTypes.createPolicy === 'function') {
423
- policy = window.trustedTypes.createPolicy('pendo', policyOptions);
424
- }
425
- else {
426
- policy = policyOptions;
427
- }
428
- pendo.trustedTypesPolicy = policy;
429
- }
430
- return policy;
431
- }
432
-
433
433
  function escapeRegExp(string) {
434
434
  if (!string)
435
435
  return;
436
436
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
437
437
  }
438
438
 
439
- var STAGING_SERVER_HASHES = 'stagingServerHashes';
440
- var STAGING_DOMAINS = 'stagingDomains';
441
- var pendo$1;
442
- var _pendoConfig = {};
443
- function loadAsModule(config) {
444
- return config.loadAsModule;
445
- }
446
- function createPendoObject(config) {
447
- var windowOrMountPoint = loadAsModule(config) ? {} : window;
448
- const agentScriptTag = document.currentScript || findAgentScriptTag(document.getElementsByTagName('script'), config.apiKey);
449
- let globalKey = config.pendoGlobalKey || 'pendo';
450
- if (agentScriptTag) {
451
- globalKey = agentScriptTag.getAttribute('data-pendo-global-key') || globalKey;
452
- config.pendoGlobalKey = globalKey;
453
- }
454
- pendo$1 = windowOrMountPoint[globalKey] = windowOrMountPoint[globalKey] || {};
455
- return pendo$1;
456
- }
457
- function isStagingEnvironment(config) {
458
- return config.environmentName === 'staging';
459
- }
460
- function isMinifiedAgent(config) {
461
- return !config.unminified;
462
- }
463
- function isExtension(config) {
464
- return config.installType === 'extension'; // can't use isExtensionAgent() here because of circular dependency and ConfigReader not being ready yet
465
- }
466
- function shouldUseStagingAgent(config) {
467
- return !isExtension(config) && !isStagingEnvironment(config) && isStagingServer(config);
468
- }
469
- function shouldUseUnminifiedAgent(config, debuggingEnabled) {
470
- return !isExtension(config) && isMinifiedAgent(config) && debuggingEnabled;
471
- }
472
- function findAgentScriptTag(scripts = [], apiKey) {
473
- const regex = /^https:\/\/[\w\-.]*cdn[\w\-.]*\.(pendo-dev\.com|pendo\.io)\/agent\/static\/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}|PENDO_API_KEY)\/pendo\.js$/g;
474
- for (let i = 0; i < scripts.length; i++) {
475
- const script = scripts[i];
476
- if (regex.test(script.src) || script.id === `pendo-${apiKey}`) {
477
- return script;
478
- }
479
- }
480
- }
481
- function buildCDNStagingAgentUrl(scripts = []) {
482
- const agentScript = findAgentScriptTag(scripts);
483
- if (agentScript) {
484
- return agentScript.src.replace(/\.js$/, '-staging.js');
485
- }
486
- }
487
- function getAlternateAgentUrl(config, useStaging, debuggingEnabled) {
488
- var agentUrl = buildCDNStagingAgentUrl(document.getElementsByTagName('script')) || config.stagingAgentUrl;
489
- if (debuggingEnabled) {
490
- var suffix = 'unminified.js';
491
- if (useStaging) {
492
- suffix = 'staging-' + suffix;
493
- }
494
- agentUrl = agentUrl && agentUrl.replace(/staging\.js$/, suffix);
495
- }
496
- return agentUrl;
439
+ // Matching a host or url against configured patterns and hashes. Shared by the
440
+ // preamble and by the agent, so it must stay as light on imports as the preamble
441
+ // is - nothing from the core SDK belongs in here.
442
+ function getHash(str) {
443
+ return b64.uint8ToBase64(sha1
444
+ .create()
445
+ .update(str)
446
+ .digest());
497
447
  }
498
- function urlMatchesPatterns(url, patterns = [], hashes = []) {
448
+ // A configured pattern is meant for a whole origin, but test() is a substring
449
+ // search, so on its own it also accepts a look-alike like
450
+ // "https://app.customer.com.attacker.tld". Rebuild it anchored. The group keeps
451
+ // a top-level alternation from escaping the anchors, and the optional port keeps
452
+ // a pattern that doesn't spell one out matching an origin that carries one. A
453
+ // pattern ending in its own '$' opts out of the port, having already said where
454
+ // it ends.
455
+ //
456
+ // Nothing here makes room for a missing scheme. allowedOriginServers is tested
457
+ // against a full origin and only ever comes from the Pendo config, and a string
458
+ // entry is already compiled as ^value$, so a scheme-less entry has never
459
+ // matched anything. A scheme-less glob only did because it wasn't anchored,
460
+ // which is the bug being fixed rather than behaviour to keep.
461
+ //
462
+ // 'm' is dropped rather than carried over, since it would let ^ and $ match at
463
+ // a line break instead of at the ends.
464
+ //
465
+ // Compiled forms are kept by source: isTrustedOrigin runs on every postMessage,
466
+ // and this would otherwise build a RegExp per pattern per call. A plain object
467
+ // rather than a Map or WeakMap - neither exists in IE, and nothing here is
468
+ // polyfilled.
469
+ var anchoredPatterns = {};
470
+ function anchorToWholeOrigin(regexp) {
471
+ var flags = regexp.ignoreCase ? 'i' : '';
472
+ var key = 're:' + flags + ':' + regexp.source;
473
+ if (!anchoredPatterns[key]) {
474
+ anchoredPatterns[key] = new RegExp('^(?:' + regexp.source + ')(?::\\d+)?$', flags);
475
+ }
476
+ return anchoredPatterns[key];
477
+ }
478
+ function urlMatchesPatterns(url, patterns = [], hashes = [], anchorPatterns = true) {
499
479
  if (!url)
500
480
  return false;
501
481
  if (hashes.length > 0) {
@@ -520,6 +500,9 @@ function urlMatchesPatterns(url, patterns = [], hashes = []) {
520
500
  pattern = pattern.value;
521
501
  }
522
502
  }
503
+ if (anchorPatterns && pattern instanceof RegExp) {
504
+ pattern = anchorToWholeOrigin(pattern);
505
+ }
523
506
  if (typeof pattern === 'string') {
524
507
  // String entries are literal origins/hosts, not patterns. Escape
525
508
  // them so a '.' can't act as a wildcard and match a look-alike domain.
@@ -532,6 +515,15 @@ function urlMatchesPatterns(url, patterns = [], hashes = []) {
532
515
  }
533
516
  return false;
534
517
  }
518
+ /**
519
+ * The host part of an origin, or the input unchanged when it carries no scheme.
520
+ * Allowed-origin entries are stored however they were provided - the host picker
521
+ * offers bare hosts, while Pendo's own entries and older hand-entered ones are
522
+ * full origins - so matching tries both forms rather than rewriting either.
523
+ */
524
+ function stripScheme$1(origin) {
525
+ return String(origin || '').replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
526
+ }
535
527
  function domainSuffixes(host) {
536
528
  var hostname = (host || '').split(':')[0].toLowerCase();
537
529
  if (!hostname || /^[\d.]+$/.test(hostname) || hostname.indexOf(':') >= 0)
@@ -543,7 +535,7 @@ function domainSuffixes(host) {
543
535
  }
544
536
  return suffixes;
545
537
  }
546
- function hostMatchesStagingDomains(host, domains = []) {
538
+ function hostMatchesDomains(host, domains = []) {
547
539
  if (!domains.length)
548
540
  return false;
549
541
  var suffixes = domainSuffixes(host);
@@ -553,6 +545,79 @@ function hostMatchesStagingDomains(host, domains = []) {
553
545
  }
554
546
  return false;
555
547
  }
548
+
549
+ var STAGING_SERVER_HASHES = 'stagingServerHashes';
550
+ var STAGING_DOMAINS = 'stagingDomains';
551
+ var pendo$1;
552
+ var _pendoConfig = {};
553
+ function loadAsModule(config) {
554
+ return config.loadAsModule;
555
+ }
556
+ function createPendoObject(config) {
557
+ var windowOrMountPoint = loadAsModule(config) ? {} : window;
558
+ const agentScriptTag = document.currentScript || findAgentScriptTag(document.getElementsByTagName('script'), config.apiKey);
559
+ let globalKey = config.pendoGlobalKey || 'pendo';
560
+ if (agentScriptTag) {
561
+ globalKey = agentScriptTag.getAttribute('data-pendo-global-key') || globalKey;
562
+ config.pendoGlobalKey = globalKey;
563
+ }
564
+ pendo$1 = windowOrMountPoint[globalKey] = windowOrMountPoint[globalKey] || {};
565
+ return pendo$1;
566
+ }
567
+ function isStagingEnvironment(config) {
568
+ return config.environmentName === 'staging';
569
+ }
570
+ function isMinifiedAgent(config) {
571
+ return !config.unminified;
572
+ }
573
+ function isExtension(config) {
574
+ return config.installType === 'extension'; // can't use isExtensionAgent() here because of circular dependency and ConfigReader not being ready yet
575
+ }
576
+ function shouldUseStagingAgent(config) {
577
+ return !isExtension(config) && !isStagingEnvironment(config) && isStagingServer(config);
578
+ }
579
+ function shouldUseUnminifiedAgent(config, debuggingEnabled) {
580
+ return !isExtension(config) && isMinifiedAgent(config) && debuggingEnabled;
581
+ }
582
+ var CDN_AGENT_URL = /^https:\/\/[\w\-.]*cdn[\w\-.]*\.(pendo-dev\.com|pendo\.io)\/agent\/static\/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}|PENDO_API_KEY)\/pendo\.js$/;
583
+ function isCDNAgentScript(script) {
584
+ return CDN_AGENT_URL.test(script.src);
585
+ }
586
+ function findAgentScriptTag(scripts = [], apiKey) {
587
+ for (let i = 0; i < scripts.length; i++) {
588
+ const script = scripts[i];
589
+ if (isCDNAgentScript(script) || script.id === `pendo-${apiKey}`) {
590
+ return script;
591
+ }
592
+ }
593
+ }
594
+ function findCDNAgentScriptTag(scripts = []) {
595
+ for (let i = 0; i < scripts.length; i++) {
596
+ if (isCDNAgentScript(scripts[i])) {
597
+ return scripts[i];
598
+ }
599
+ }
600
+ }
601
+ // Only a src that passed the CDN allowlist may be rewritten into an alternate
602
+ // agent URL. Matching on id here would let any <script> in the page pick the
603
+ // script that replaces the agent.
604
+ function buildCDNStagingAgentUrl(scripts = []) {
605
+ const agentScript = findCDNAgentScriptTag(scripts);
606
+ if (agentScript) {
607
+ return agentScript.src.replace(/\.js$/, '-staging.js');
608
+ }
609
+ }
610
+ function getAlternateAgentUrl(config, useStaging, debuggingEnabled) {
611
+ var agentUrl = buildCDNStagingAgentUrl(document.getElementsByTagName('script')) || config.stagingAgentUrl;
612
+ if (debuggingEnabled) {
613
+ var suffix = 'unminified.js';
614
+ if (useStaging) {
615
+ suffix = 'staging-' + suffix;
616
+ }
617
+ agentUrl = agentUrl && agentUrl.replace(/staging\.js$/, suffix);
618
+ }
619
+ return agentUrl;
620
+ }
556
621
  function isStagingServer(config, location) {
557
622
  if (!config) {
558
623
  config = getPendoConfig();
@@ -566,14 +631,10 @@ function isStagingServer(config, location) {
566
631
  if (!location) {
567
632
  location = window.location;
568
633
  }
569
- return urlMatchesPatterns(location.host, config.stagingServers, config[STAGING_SERVER_HASHES]) ||
570
- hostMatchesStagingDomains(location.host, config[STAGING_DOMAINS]);
571
- }
572
- function getHash(str) {
573
- return b64.uint8ToBase64(sha1
574
- .create()
575
- .update(str)
576
- .digest());
634
+ // Unanchored: staging globs are matched against location.host and don't
635
+ // account for a port, and picking the staging agent isn't a trust decision.
636
+ return urlMatchesPatterns(location.host, config.stagingServers, config[STAGING_SERVER_HASHES], false) ||
637
+ hostMatchesDomains(location.host, config[STAGING_DOMAINS]);
577
638
  }
578
639
  var METHODS_TO_CAPTURE = [
579
640
  'initialize',
@@ -2793,6 +2854,15 @@ function deepCopy(obj) {
2793
2854
  }
2794
2855
  }
2795
2856
 
2857
+ // Markup alone can define window.pendo_options (DOM clobbering via an element's id), so only
2858
+ // accept it when it's a plain object the page actually assigned
2859
+ function getLegacyGlobalSnippetOptions() {
2860
+ var options = window.pendo_options;
2861
+ if (Object.prototype.toString.call(options) !== '[object Object]')
2862
+ return;
2863
+ return options;
2864
+ }
2865
+
2796
2866
  var originalOptions;
2797
2867
  var ConfigReader = (function () {
2798
2868
  // imports
@@ -2869,6 +2939,7 @@ var ConfigReader = (function () {
2869
2939
  addOption('additionalPublicAppIds', [PENDO_CONFIG_SRC, SNIPPET_SRC], undefined, undefined, ['additionalApiKeys']);
2870
2940
  addOption('allowedOriginServers');
2871
2941
  addOption('allowedOriginServerHashes');
2942
+ addOption('enforceAllowedOriginServers', [SNIPPET_SRC, PENDO_CONFIG_SRC], false);
2872
2943
  addOption('allowMixedApplicationFrames', [SNIPPET_SRC, PENDO_CONFIG_SRC], true);
2873
2944
  addOption('allowMixedApiKeyFrames', [SNIPPET_SRC, PENDO_CONFIG_SRC], true);
2874
2945
  /**
@@ -3120,16 +3191,16 @@ var ConfigReader = (function () {
3120
3191
  addOption('pendoGlobalKey', [PENDO_CONFIG_SRC]);
3121
3192
  /**
3122
3193
  * By default, if there are multiple frames on the page that have the web SDK installed, they will communicate
3123
- * using `window.postMessage`. If set to `true` the web SDK will instead use the browser's Broadcast Channel
3124
- * API to communicate. This will only work when all frames use the same domain.
3194
+ * using the browser's Broadcast Channel API. This will only work when all frames use the same domain. If set
3195
+ * to `false` the web SDK will instead use `window.postMessage`, which also works across domains.
3125
3196
  *
3126
3197
  * @access public
3127
3198
  * @category Config/Core
3128
3199
  * @name preferBroadcastChannel
3129
- * @default false
3200
+ * @default true
3130
3201
  * @type {boolean}
3131
3202
  */
3132
- addOption('preferBroadcastChannel', [SNIPPET_SRC, PENDO_CONFIG_SRC], false);
3203
+ addOption('preferBroadcastChannel', [SNIPPET_SRC, PENDO_CONFIG_SRC], true);
3133
3204
  /**
3134
3205
  * By default, the web SDK will scan the page every 500ms to determine if any new badges or guides are eligible.
3135
3206
  * To improve idle performance, this can be set to `true` and then the web SDK will instead use a mutation
@@ -3144,17 +3215,17 @@ var ConfigReader = (function () {
3144
3215
  addOption('preferMutationObserver', [SNIPPET_SRC, PENDO_CONFIG_SRC], false);
3145
3216
  addOption('preventCookieRefresh');
3146
3217
  /**
3147
- * By default, the web SDK adds an `unload` event listener to the page to make sure it sends events upon
3148
- * navigation. If set to `true`, the web SDK will instead use the `visibilitychange` event listener and send
3149
- * events when the visibility is changing to `"hidden"`.
3218
+ * By default, the web SDK uses the `visibilitychange` event listener and sends events when the visibility is
3219
+ * changing to `"hidden"`. If set to `false`, the web SDK will instead add an `unload` event listener to the
3220
+ * page to make sure it sends events upon navigation.
3150
3221
  *
3151
3222
  * @access public
3152
3223
  * @category Config/Core
3153
3224
  * @name preventUnloadListener
3154
- * @default false
3225
+ * @default true
3155
3226
  * @type {boolean}
3156
3227
  */
3157
- addOption('preventUnloadListener', [SNIPPET_SRC, PENDO_CONFIG_SRC], false);
3228
+ addOption('preventUnloadListener', [SNIPPET_SRC, PENDO_CONFIG_SRC], true);
3158
3229
  addOption('promotedWebSDKMetadataConfigurations', [PENDO_CONFIG_SRC], undefined, undefined, ['promotedAgentMetadataConfigurations']);
3159
3230
  /**
3160
3231
  * An array of strings or a function that returns an array of strings. The web SDK will then strip any query
@@ -3419,6 +3490,18 @@ var ConfigReader = (function () {
3419
3490
  */
3420
3491
  addOption('disableGuidePseudoStyles', [SNIPPET_SRC, PENDO_CONFIG_SRC], false);
3421
3492
  addOption('disableImportantStyleAttributes', [SNIPPET_SRC, PENDO_CONFIG_SRC], false);
3493
+ /**
3494
+ * When a guide has a backdrop enabled, by default elements outside the guide are hidden from assistive
3495
+ * technology using `aria-hidden`. If `true`, the web SDK will instead apply the `inert` attribute,
3496
+ * which also removes those elements from focus and pointer interaction.
3497
+ *
3498
+ * @access public
3499
+ * @category Config/Guides
3500
+ * @name useInertForBackdrop
3501
+ * @default false
3502
+ * @type {boolean}
3503
+ */
3504
+ addOption('useInertForBackdrop', [SNIPPET_SRC, PENDO_CONFIG_SRC], false);
3422
3505
  /**
3423
3506
  * Content of badge and target element activated guides will be prefetched on load to improve guide display
3424
3507
  * time unless set to `true`.
@@ -3676,7 +3759,7 @@ var ConfigReader = (function () {
3676
3759
  initializeOptions();
3677
3760
  var sourceGetters = {};
3678
3761
  sourceGetters[SNIPPET_SRC] = function () {
3679
- return { lookup: originalOptions || window.pendo_options, name: SNIPPET_SRC };
3762
+ return { lookup: originalOptions || getLegacyGlobalSnippetOptions(), name: SNIPPET_SRC };
3680
3763
  };
3681
3764
  sourceGetters[PENDO_CONFIG_SRC] = function () {
3682
3765
  var lookup = getPendoConfig();
@@ -3926,17 +4009,6 @@ function shouldWrapHashChange() {
3926
4009
  function isMobileUserAgent() {
3927
4010
  return (/Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i).test(getUA());
3928
4011
  }
3929
- // WICG inert polyfills through 3.1.0 install their MutationObserver-driven manager even where
3930
- // the browser supports inert natively, and inject #inert-style when they do. With one active,
3931
- // inerting a large subtree costs a full tree walk plus a subtree observer for as long as the
3932
- // attribute stays, so treat the page as not supporting inert.
3933
- function supportsInert() {
3934
- const descriptor = window.HTMLElement && _.isFunction(Object.getOwnPropertyDescriptor) &&
3935
- Object.getOwnPropertyDescriptor(window.HTMLElement.prototype, 'inert');
3936
- const isNative = !!(descriptor && descriptor.get && isNativeCode(descriptor.get));
3937
- const isPolyfilled = !!document.getElementById('inert-style');
3938
- return isNative && !isPolyfilled;
3939
- }
3940
4012
  function isSafari(userAgent = getUA()) {
3941
4013
  return userAgent.indexOf('Safari') >= 0 && !isChromium(userAgent);
3942
4014
  }
@@ -3949,7 +4021,6 @@ function isChromium(userAgent = getUA()) {
3949
4021
  const sniffer = {
3950
4022
  supportsHistoryApi: shouldWrapNativeHistory,
3951
4023
  supportsHashChange: shouldWrapHashChange,
3952
- supportsInert,
3953
4024
  hasEvent(event) {
3954
4025
  // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
3955
4026
  // it. In particular the event is not fired when backspace or delete key are pressed or
@@ -4058,8 +4129,8 @@ let SERVER = '';
4058
4129
  let ASSET_HOST = '';
4059
4130
  let ASSET_PATH = '';
4060
4131
  let DESIGNER_SERVER = '';
4061
- let VERSION = '2.342.0_';
4062
- let PACKAGE_VERSION = '2.342.0';
4132
+ let VERSION = '2.344.0_';
4133
+ let PACKAGE_VERSION = '2.344.0';
4063
4134
  let LOADER = 'xhr';
4064
4135
  /* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
4065
4136
  /**
@@ -4476,15 +4547,36 @@ class LogEmitter extends EventTarget {
4476
4547
  }
4477
4548
  const log = new LogEmitter();
4478
4549
 
4479
- /** Key used to lookup the consent collection policy in the pendo configuration. */
4480
4550
  const CONSENT_COLLECTION = 'cookieConsentCollection';
4481
- /** Key used to lookup the analytics events in the consent collection policy. */
4482
4551
  const ANALYTICS = 'analytics';
4483
- /** Key used to lookup the guide analytics events in the consent collection policy. */
4484
4552
  const GUIDE_ANALYTICS = 'guideAnalytics';
4485
- /** Key used to lookup the text collection mode in the consent collection policy. */
4486
4553
  const TEXT_COLLECTION = 'textCollection';
4487
4554
  const TEXT_COLLECTION_LEVELS = ['none', 'whitelist', 'all'];
4555
+ const ANALYTICS_EVENT_TYPES = {
4556
+ agentResponse: 'agent_response',
4557
+ clicks: 'click',
4558
+ fieldChanges: 'change',
4559
+ focus: 'focus',
4560
+ formSubmits: 'submit',
4561
+ identify: 'identify',
4562
+ meta: 'meta',
4563
+ objectMetadata: 'objectMetadata',
4564
+ pageLoads: 'load',
4565
+ prompt: 'prompt',
4566
+ trackEvents: 'track',
4567
+ userReaction: 'user_reaction'
4568
+ };
4569
+ const GUIDE_EVENT_TYPES = [
4570
+ 'guideActivity',
4571
+ 'guideAdvanced',
4572
+ 'guideDismissed',
4573
+ 'guideNotSeen',
4574
+ 'guideSeen',
4575
+ 'guideSnoozeCanceled',
4576
+ 'guideSnoozed',
4577
+ 'guideTimeout',
4578
+ 'pollResponse'
4579
+ ];
4488
4580
  let enabledProducts = null;
4489
4581
  function getClassification() {
4490
4582
  return ConfigReader.get(CONSENT_COLLECTION) || {};
@@ -4494,6 +4586,9 @@ function toTierList(tiers) {
4494
4586
  return [tiers];
4495
4587
  return _.isArray(tiers) ? tiers : [];
4496
4588
  }
4589
+ function sameConsentTiers(a, b) {
4590
+ return _.isEqual(toTierList(a).slice().sort(), toTierList(b).slice().sort());
4591
+ }
4497
4592
  function warnOnUnconfiguredTiers(grantedTiers, classification) {
4498
4593
  _.each(grantedTiers, function (tier) {
4499
4594
  if (_.isUndefined(classification[tier])) {
@@ -4504,30 +4599,62 @@ function warnOnUnconfiguredTiers(grantedTiers, classification) {
4504
4599
  function moreTextPermissive(a, b) {
4505
4600
  return TEXT_COLLECTION_LEVELS.indexOf(b) > TEXT_COLLECTION_LEVELS.indexOf(a) ? b : a;
4506
4601
  }
4507
- function orBoolean(a, b) {
4508
- return a || Boolean(b);
4602
+ function toTextCollectionLevel(mode) {
4603
+ return _.contains(TEXT_COLLECTION_LEVELS, mode) ? mode : TEXT_COLLECTION_LEVELS[0];
4509
4604
  }
4510
- function unionArrays(a, b) {
4511
- return _.union(a, b);
4605
+ function toAnalyticsEventTypes(eventCollection) {
4606
+ return _.reduce(ANALYTICS_EVENT_TYPES, function (types, eventType, key) {
4607
+ if (eventCollection[key] === true) {
4608
+ types.push(eventType);
4609
+ }
4610
+ return types;
4611
+ }, []);
4612
+ }
4613
+ function normalizeTier(tier) {
4614
+ const { visitorIdentity = {}, analytics = {}, guides = {}, sessionReplay = {} } = tier || {};
4615
+ const eventCollection = analytics.eventCollection || {};
4616
+ const analyticsEnabled = analytics.enabled === true;
4617
+ return {
4618
+ identity: visitorIdentity.trackVisitors === true,
4619
+ guides: guides.enabled === true,
4620
+ replay: sessionReplay.enabled === true,
4621
+ utm: analyticsEnabled && eventCollection.utmParameters === true,
4622
+ analytics: analyticsEnabled ? toAnalyticsEventTypes(eventCollection) : [],
4623
+ guideAnalytics: analyticsEnabled && eventCollection.guideServing === true ? GUIDE_EVENT_TYPES.slice() : [],
4624
+ textCollection: analyticsEnabled ? toTextCollectionLevel(eventCollection.textCollectionMode) : TEXT_COLLECTION_LEVELS[0]
4625
+ };
4626
+ }
4627
+ function deniedProducts() {
4628
+ return {
4629
+ identity: false,
4630
+ guides: false,
4631
+ replay: false,
4632
+ utm: false,
4633
+ [ANALYTICS]: [],
4634
+ [GUIDE_ANALYTICS]: [],
4635
+ [TEXT_COLLECTION]: TEXT_COLLECTION_LEVELS[0]
4636
+ };
4637
+ }
4638
+ function combineTiers(resolved, granted) {
4639
+ return {
4640
+ identity: resolved.identity || granted.identity,
4641
+ guides: resolved.guides || granted.guides,
4642
+ replay: resolved.replay || granted.replay,
4643
+ utm: resolved.utm || granted.utm,
4644
+ [ANALYTICS]: _.union(resolved[ANALYTICS], granted[ANALYTICS]),
4645
+ [GUIDE_ANALYTICS]: _.union(resolved[GUIDE_ANALYTICS], granted[GUIDE_ANALYTICS]),
4646
+ [TEXT_COLLECTION]: moreTextPermissive(resolved[TEXT_COLLECTION], granted[TEXT_COLLECTION])
4647
+ };
4512
4648
  }
4513
- // How a granted tier's value for a product combines with what's been resolved so far.
4514
- // `initial` doubles as what an ungranted tier contributes -- an unlisted product falls
4515
- // back to DEFAULT_RESOLVER, the plain "any granted tier turns it on" boolean rule.
4516
- const RESOLVERS = {
4517
- [TEXT_COLLECTION]: { initial: TEXT_COLLECTION_LEVELS[0], combine: moreTextPermissive },
4518
- [ANALYTICS]: { initial: [], combine: unionArrays },
4519
- [GUIDE_ANALYTICS]: { initial: [], combine: unionArrays }
4520
- };
4521
- const DEFAULT_RESOLVER = { initial: false, combine: orBoolean };
4522
4649
  /**
4523
- * Resolves the granted tiers against the configured product classification.
4650
+ * Resolves the tiers a visitor consented to into what each product is allowed to do.
4524
4651
  *
4525
- * A product the classification mentions anywhere starts at its resolver's `initial` value
4526
- * -- a tier the visitor hasn't opted into contributes that same `initial` value, so it has
4527
- * no say over the product, even to explicitly allow something. Only a granted tier's actual
4528
- * value is combined in, so opting into an additional tier only ever adds capability. Most
4529
- * products are plain booleans (DEFAULT_RESOLVER); `textCollection`, `analytics`, and
4530
- * `guideAnalytics` have their own entries in RESOLVERS since none of them is a boolean.
4652
+ * Every product starts denied and only granted tiers contribute, so a tier the visitor
4653
+ * hasn't opted into has no say -- it can't deny anything, and it can't allow anything.
4654
+ *
4655
+ * A subscription with no cookie consent configured resolves to an empty map instead, which
4656
+ * every read below treats as unconstrained: consent that was never set up can't deny
4657
+ * anything either.
4531
4658
  *
4532
4659
  * @access private
4533
4660
  * @param {string | string[]} tiers tier or tiers the visitor consented to
@@ -4536,26 +4663,22 @@ const DEFAULT_RESOLVER = { initial: false, combine: orBoolean };
4536
4663
  function applyConsentTiers(tiers) {
4537
4664
  const cookieConsentCollection = getClassification();
4538
4665
  const consentedCookieTiers = toTierList(tiers);
4539
- const resolved = {};
4540
4666
  warnOnUnconfiguredTiers(consentedCookieTiers, cookieConsentCollection);
4541
- _.each(cookieConsentCollection, function (products, tier) {
4542
- var isGranted = _.contains(consentedCookieTiers, tier);
4543
- _.each(products, function (level, product) {
4544
- var resolver = RESOLVERS[product] || DEFAULT_RESOLVER;
4545
- var current = _.has(resolved, product) ? resolved[product] : resolver.initial;
4546
- var contributed = isGranted ? level : resolver.initial;
4547
- resolved[product] = resolver.combine(current, contributed);
4548
- });
4549
- });
4550
- enabledProducts = resolved;
4667
+ if (_.isEmpty(cookieConsentCollection)) {
4668
+ enabledProducts = {};
4669
+ return getEnabledProducts();
4670
+ }
4671
+ enabledProducts = _.reduce(consentedCookieTiers, function (resolved, tier) {
4672
+ const granted = cookieConsentCollection[tier];
4673
+ return granted ? combineTiers(resolved, normalizeTier(granted)) : resolved;
4674
+ }, deniedProducts());
4551
4675
  return getEnabledProducts();
4552
4676
  }
4553
4677
  function getEnabledProducts() {
4554
4678
  return enabledProducts ? _.extend({}, enabledProducts) : null;
4555
4679
  }
4556
4680
  /**
4557
- * A product is enabled unless a tier has explicitly resolved it to false -- with no cookie
4558
- * consent configured at all, or a product no tier ever mentions, there's nothing to deny.
4681
+ * A product is enabled only if some granted tier turned it on or cookie consent has not been set.
4559
4682
  *
4560
4683
  * @access private
4561
4684
  * @param {string} product
@@ -4569,6 +4692,12 @@ function isProductEnabled(product) {
4569
4692
  function isTrackingConsentDenied() {
4570
4693
  return !isProductEnabled('identity');
4571
4694
  }
4695
+ function isGuideConsentDenied() {
4696
+ return !isProductEnabled('guides');
4697
+ }
4698
+ function isUtmConsentDenied() {
4699
+ return !isProductEnabled('utm');
4700
+ }
4572
4701
  /**
4573
4702
  * Session replay is denied whenever tracking itself is denied (DNT) -- a visitor
4574
4703
  * who can't be identified shouldn't have their session recorded either -- or when a
@@ -4604,6 +4733,9 @@ function hasConsentForAnalyticsEvent(type) {
4604
4733
  function hasConsentForGuideEvent(type) {
4605
4734
  return hasConsentForProduct(type, GUIDE_ANALYTICS);
4606
4735
  }
4736
+ function resetConsentPolicy() {
4737
+ enabledProducts = null;
4738
+ }
4607
4739
 
4608
4740
  const allowKeys = function () {
4609
4741
  var allowKeys = ConfigReader.get('storage.allowKeys') || '*';
@@ -5915,7 +6047,6 @@ var Events = (function () {
5915
6047
  new EventType('recording:unpaused', [LIFECYCLE]),
5916
6048
  new EventType('recording:paused', [LIFECYCLE]),
5917
6049
  new EventType('consent:changed', [LIFECYCLE]),
5918
- new EventType('identifyRequested', [DEBUG, LIFECYCLE]),
5919
6050
  new EventType('childFrameJoined', [FRAMES]),
5920
6051
  new EventType('tabIdChanged:self', [DEBUG, LIFECYCLE])
5921
6052
  ];
@@ -11994,6 +12125,30 @@ _.extend(dom, {
11994
12125
  });
11995
12126
  _.extend(dom.prototype, DomEvent.$, DomQuery.$);
11996
12127
 
12128
+ // location.origin only arrived in IE11, and is missing again under the older
12129
+ // document modes IE11 can emulate
12130
+ function readOrigin(location) {
12131
+ if (location.origin !== undefined)
12132
+ return location.origin;
12133
+ return location.protocol + '//' + location.host;
12134
+ }
12135
+ function getLocationOrigin(window) {
12136
+ const strNull = 'null';
12137
+ const origin = readOrigin(window.location);
12138
+ if (origin === strNull && window.parent && window.parent != window) {
12139
+ try {
12140
+ const parentOrigin = readOrigin(window.parent.location);
12141
+ if (parentOrigin && parentOrigin !== strNull) {
12142
+ return parentOrigin;
12143
+ }
12144
+ }
12145
+ catch (e) {
12146
+ // cross-origin ancestor; cannot read top location
12147
+ }
12148
+ }
12149
+ return origin;
12150
+ }
12151
+
11997
12152
  // Domains that Pendo owns, will be swapped in by build patches
11998
12153
  const pendoDomainPatterns = [
11999
12154
  /^https:\/\/((adopt|oem)\.)?((us1)\.)?(app|via|adopt|cdn|oem|novus-api|novus|portal)(\.(au|eu|gov|hsbc|jpn|tv))?\.pendo\.io$/,
@@ -12001,11 +12156,34 @@ const pendoDomainPatterns = [
12001
12156
  /^https:\/\/hotfix-(ops|app)-([0-9]+-dot-)(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-tv|pendo-us1)\.appspot\.com$/,
12002
12157
  /^https:\/\/(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-tv|pendo-us1)-static\.storage\.googleapis\.com$/
12003
12158
  ]; // eslint-disable-line web-sdk-eslint-rules/no-gulp-env-references
12159
+ /**
12160
+ * A leading dot marks a subtree: ".foo.com" covers foo.com and everything below
12161
+ * it, while "foo.com" stays that one host and is matched exactly above.
12162
+ *
12163
+ * Keeping the dot in what we compare is what tells the two apart, so subtree
12164
+ * entries can share allowedOriginServers / allowedOriginServerHashes instead of
12165
+ * needing a list of their own. stagingDomains needs a separate list only because
12166
+ * it strips the dot and hashes unconditionally, which allowed origins don't.
12167
+ *
12168
+ * Both forms are checked because appengine passes a non-glob name through
12169
+ * verbatim: as a quoted string when the subscription doesn't hash its servers,
12170
+ * as sha1 of that same string when it does.
12171
+ *
12172
+ * domainSuffixes drops any port and never yields a single label, so ".com" can
12173
+ * never match.
12174
+ */
12175
+ function matchesSubtreeEntry(host, servers, hashes) {
12176
+ var suffixes = domainSuffixes(stripScheme$1(host));
12177
+ return _.any(suffixes, function (suffix) {
12178
+ var entry = '.' + suffix;
12179
+ return _.contains(servers, entry) || _.contains(hashes, getHash(entry));
12180
+ });
12181
+ }
12004
12182
  function isTrustedOrigin(host) {
12005
12183
  if (!host)
12006
12184
  return false;
12007
12185
  // for CNAME, trust the top-level origin
12008
- if (host === window.location.origin)
12186
+ if (host === readOrigin(window.location))
12009
12187
  return true;
12010
12188
  // Trust the configured hosts
12011
12189
  if (host === getDataHost())
@@ -12021,7 +12199,21 @@ function isTrustedOrigin(host) {
12021
12199
  if (host === fullHostname)
12022
12200
  return true;
12023
12201
  }
12024
- if (urlMatchesPatterns(host, ConfigReader.get('allowedOriginServers', []), ConfigReader.get('allowedOriginServerHashes', [])))
12202
+ // Entries are matched as both a full origin and a bare host, because that is
12203
+ // how they were stored: the host picker offers names straight off servername,
12204
+ // while Pendo's own entries and older hand-entered ones carry a scheme. A
12205
+ // string entry compiles to ^value$, so a bare host has never matched the full
12206
+ // origin these callers pass, and only a glob did - by accident, its leading
12207
+ // '.*' swallowing the scheme. Testing both forms keeps existing origin
12208
+ // entries pinned to their scheme and lets a bare host match whichever scheme
12209
+ // it arrives on, without rewriting anything already provisioned.
12210
+ var allowedServers = ConfigReader.get('allowedOriginServers', []);
12211
+ var allowedHashes = ConfigReader.get('allowedOriginServerHashes', []);
12212
+ if (urlMatchesPatterns(host, allowedServers, allowedHashes))
12213
+ return true;
12214
+ if (urlMatchesPatterns(stripScheme$1(host), allowedServers, allowedHashes))
12215
+ return true;
12216
+ if (matchesSubtreeEntry(host, allowedServers, allowedHashes))
12025
12217
  return true;
12026
12218
  return _.any(pendoDomainPatterns, function (pattern) {
12027
12219
  return pattern.test(host);
@@ -14209,15 +14401,13 @@ const internalEvents = {
14209
14401
  onEventCaptured: 1,
14210
14402
  segmentFlagsUpdated: 1,
14211
14403
  segmentFlagsReady: 1,
14212
- segmentFlagsError: 1,
14213
- identifyRequested: 1
14404
+ segmentFlagsError: 1
14214
14405
  };
14215
14406
  const browserEvents = {
14216
14407
  ready: 1,
14217
14408
  segmentFlagsUpdated: 1,
14218
14409
  segmentFlagsReady: 1,
14219
- segmentFlagsError: 1,
14220
- identifyRequested: 1
14410
+ segmentFlagsError: 1
14221
14411
  };
14222
14412
  const supportedPublicEvents = [
14223
14413
  'ready',
@@ -14231,8 +14421,7 @@ const supportedPublicEvents = [
14231
14421
  'segmentFlagsError',
14232
14422
  'validateGuide',
14233
14423
  'validateLauncher',
14234
- 'validateGlobalScript',
14235
- 'identifyRequested'
14424
+ 'validateGlobalScript'
14236
14425
  ];
14237
14426
  function forwardInternalEvents(Events) {
14238
14427
  const unsubscribes = _.reduce(supportedPublicEvents, function (unsubscribes, eventName) {
@@ -21118,8 +21307,11 @@ function isHidden(document) {
21118
21307
  const visibilitychange = 'visibilitychange';
21119
21308
  const pagehide = 'pagehide';
21120
21309
  const unload = 'unload';
21310
+ function supportsPageHide() {
21311
+ return 'onpagehide' in window;
21312
+ }
21121
21313
  function onPageHidden(callback, useVisibilityChange) {
21122
- if (useVisibilityChange) {
21314
+ if (useVisibilityChange && supportsPageHide()) {
21123
21315
  const unlisten = [];
21124
21316
  const onChange = () => {
21125
21317
  if (isHidden(document)) {
@@ -22161,14 +22353,29 @@ function ClickActivation() {
22161
22353
  }
22162
22354
  stopEvent(evt);
22163
22355
  };
22356
+ // Safari does not focus the badge on mousedown, so focus leaves the app instead.
22357
+ // Apps that re-render on blur can remove the badge before mouseup, and the browser
22358
+ // then generates no click at all. Holding focus where it is avoids that (APP-168412)
22359
+ var preserveFocus = function (evt) {
22360
+ if (evt.preventDefault) {
22361
+ evt.preventDefault();
22362
+ }
22363
+ else {
22364
+ evt.returnValue = false;
22365
+ }
22366
+ };
22164
22367
  badge.after('show', function () {
22165
22368
  if (!attached) {
22166
22369
  attachEvent(badgeElem, 'click', click);
22370
+ if (badge.preserveFocusOnClick) {
22371
+ attachEvent(badgeElem, 'mousedown', preserveFocus);
22372
+ }
22167
22373
  attached = true;
22168
22374
  }
22169
22375
  });
22170
22376
  badge.after('hide', function () {
22171
22377
  detachEvent(badgeElem, 'click', click);
22378
+ detachEvent(badgeElem, 'mousedown', preserveFocus);
22172
22379
  attached = false;
22173
22380
  });
22174
22381
  return badge;
@@ -22639,7 +22846,7 @@ function stepShowingProc(guide, step) {
22639
22846
  return;
22640
22847
  }
22641
22848
  const embedSelector = _.get(guide, 'attributes.embedConfig.selector');
22642
- if (embedSelector && !dom(embedSelector).length) {
22849
+ if (embedSelector && (!dom(embedSelector).length || (guideElement && !isInDocument(guideElement[0])))) {
22643
22850
  step.hide();
22644
22851
  return;
22645
22852
  }
@@ -23603,14 +23810,14 @@ var autofocusStep = {
23603
23810
  script: autofocusRuntimeScript
23604
23811
  };
23605
23812
 
23606
- function backdropInertRuntimeScript(step, guide, pendo) {
23813
+ function backdropVisibilityRuntimeScript(step, guide, pendo) {
23607
23814
  if (pendo.designer || !step.guideElement)
23608
23815
  return;
23609
23816
  if (step.hasBackdrop) {
23610
23817
  const BuildingBlockGuides = pendo.BuildingBlocks.BuildingBlockGuides;
23611
23818
  const guideElement = step.guideElement[0];
23612
23819
  const excludeElements = getExcludedElements(step);
23613
- const disabledElements = sniffer.supportsInert()
23820
+ const disabledElements = ConfigReader.get('useInertForBackdrop')
23614
23821
  ? BuildingBlockGuides.addInertOutsideGuide(guideElement, [], excludeElements)
23615
23822
  : BuildingBlockGuides.addAriaHiddenOutsideGuide(guideElement, [], excludeElements);
23616
23823
  step.hiddenElements = (step.hiddenElements || []).concat(disabledElements);
@@ -23626,8 +23833,8 @@ function getExcludedElements(step) {
23626
23833
  }
23627
23834
  return excludeElements;
23628
23835
  }
23629
- var backdropInertStep = {
23630
- script: backdropInertRuntimeScript
23836
+ var backdropVisibilityStep = {
23837
+ script: backdropVisibilityRuntimeScript
23631
23838
  };
23632
23839
 
23633
23840
  function isTooltip({ _ }, step) {
@@ -23787,7 +23994,7 @@ function registerPostrenderScripts(runtime) {
23787
23994
  advanceOnElement,
23788
23995
  announcementMarkSeen,
23789
23996
  autofocusStep,
23790
- backdropInertStep,
23997
+ backdropVisibilityStep,
23791
23998
  repositionOnScroll,
23792
23999
  setFocusToActivationElement$1,
23793
24000
  videoLoadingAttribute
@@ -25381,8 +25588,17 @@ var initGuides = function (observer) {
25381
25588
  var { guides } = event.data[0];
25382
25589
  if (_.isUndefined(guides))
25383
25590
  return;
25384
- guides ? enableGuides() : disableGuides();
25591
+ if (guides) {
25592
+ enableGuides();
25593
+ return;
25594
+ }
25595
+ disableGuides();
25596
+ clearGuideThrottlingState();
25385
25597
  });
25598
+ if (isGuideConsentDenied()) {
25599
+ disableGuides();
25600
+ clearGuideThrottlingState();
25601
+ }
25386
25602
  teardownFns.push(onTurbolinksPageLoad(document, function onPageLoad() {
25387
25603
  if (pendoDotUrl.get() === reloadGuides.lastUrl) {
25388
25604
  // Force a reload if Turbolinks replaces the document body without changing the url
@@ -25511,6 +25727,11 @@ function disableGuides() {
25511
25727
  setGuidesDisabled(true);
25512
25728
  resetPendoUI();
25513
25729
  }
25730
+ function clearGuideThrottlingState() {
25731
+ _.each(THROTTLING_STATE, function (throttlingStorageKey) {
25732
+ agentStorage.clear(throttlingStorageKey);
25733
+ });
25734
+ }
25514
25735
  var setGuidesDelayed = function (areDelayed) {
25515
25736
  var originalOptions = ConfigReader.getLocalConfig();
25516
25737
  delete originalOptions.delayGuides;
@@ -25549,10 +25770,20 @@ var adjustActiveGuideOnResize = function () {
25549
25770
  // as the user zooms the browser APP-53281
25550
25771
  log.debug('adjustActiveGuideOnResize firing');
25551
25772
  var activeGuide = getActiveGuide();
25552
- if (activeGuide && activeGuide.step && !pendo$1.ignoreResize) {
25773
+ if (activeGuide && activeGuide.step && !pendo$1.ignoreResize && !isEditingGuideTextField(activeGuide.step)) {
25553
25774
  activeGuide.step.redraw();
25554
25775
  }
25555
25776
  };
25777
+ // Skip the resize redraw while the user is focused in a guide text field: the redraw blurs the
25778
+ // field, which on mobile dismisses the soft keyboard (APP-167261).
25779
+ var isEditingGuideTextField = function (step) {
25780
+ var active = document.activeElement;
25781
+ if (!active || (active.tagName !== 'TEXTAREA' && active.tagName !== 'INPUT')) {
25782
+ return false;
25783
+ }
25784
+ var guideEl = step.guideElement && step.guideElement[0];
25785
+ return !!guideEl && guideEl.contains(active);
25786
+ };
25556
25787
  var resizeHandler = function () {
25557
25788
  // In some browsers, it's possible for activeElement to become undefined or null
25558
25789
  var activeElementId = _.get(document, 'activeElement.id', '');
@@ -25881,6 +26112,46 @@ function stopLivePreviewRelay(onStopped) {
25881
26112
  }, relay.origin);
25882
26113
  }
25883
26114
 
26115
+ /**
26116
+ * Whether to police who may join the cross-frame channel (APP-165872).
26117
+ *
26118
+ * Off by default, and deliberately named after the setting it depends on: the
26119
+ * check trusts allowedOriginServers, which Pendo provisions per subscription,
26120
+ * so it can only be switched on for an application whose origins are actually
26121
+ * listed. With it off the relay behaves exactly as it always has.
26122
+ */
26123
+ function enforceAllowedOriginServers() {
26124
+ return ConfigReader.get('enforceAllowedOriginServers') === true;
26125
+ }
26126
+ /**
26127
+ * Top's origin, read straight off the ancestor chain. Every current browser
26128
+ * exposes location.ancestorOrigins; IE does not.
26129
+ *
26130
+ * Returns null whenever top can't be identified - no ancestorOrigins, an empty
26131
+ * chain, an opaque top that reports the literal string "null", or a location
26132
+ * that throws. Callers must treat null as "refuse", never as "trusted": an
26133
+ * application that asked for enforcement gets it everywhere, including on the
26134
+ * browsers where we have no way to look.
26135
+ */
26136
+ function readTopOrigin(window) {
26137
+ try {
26138
+ if (window === window.top)
26139
+ return readOrigin(window.location);
26140
+ var ancestors = window.location.ancestorOrigins;
26141
+ if (!ancestors || !ancestors.length)
26142
+ return null;
26143
+ // last entry is the top-most ancestor; a sandboxed or data: top reports
26144
+ // the literal string "null", which is not a usable targetOrigin. Only the
26145
+ // top matters here - an opaque ancestor in between still leaves a real
26146
+ // origin at the end of the chain.
26147
+ var top = ancestors[ancestors.length - 1];
26148
+ return (top && top !== 'null') ? top : null;
26149
+ }
26150
+ catch (e) {
26151
+ return null;
26152
+ }
26153
+ }
26154
+
25884
26155
  var REMOVE_FRAME_ACTION = 'frames/removeFrame';
25885
26156
  function tryParseJson(messageData) {
25886
26157
  if (!_.isString(messageData)) {
@@ -25942,6 +26213,12 @@ _.extend(TopFrameRelay.prototype, {
25942
26213
  if (message.data.action === 'pendo::connect') {
25943
26214
  if (!isSameWindowTree(message, this._window))
25944
26215
  return;
26216
+ // A third-party widget embedded on the page is a frame below us, so it
26217
+ // passes the tree check and frameConnect would wire its port straight
26218
+ // into the store. Who the sender is, rather than where it sits, is what
26219
+ // keeps that out (APP-165872).
26220
+ if (enforceAllowedOriginServers() && !isTrustedOrigin(message.origin))
26221
+ return;
25945
26222
  this._impl.frameConnect(message);
25946
26223
  }
25947
26224
  if (message.data.action === REMOVE_FRAME_ACTION) {
@@ -25964,10 +26241,33 @@ _.extend(TopFrameRelay.prototype, {
25964
26241
  this._impl.broadcast(message);
25965
26242
  },
25966
26243
  _connectToTop(timeout, retries, timeoutFn) {
25967
- this._impl.connectToTop({
25968
- action: 'pendo::connect',
25969
- frameId: this._store.state.frames.frameId
25970
- }, this._targetOrigin);
26244
+ // The port goes out with the connect, so it can never be taken back once an
26245
+ // untrusted top has it. Check who top is against the trusted origins before
26246
+ // handing it over, and address the message to that origin rather than '*'
26247
+ // (APP-165872).
26248
+ //
26249
+ // An application that turned enforcement on gets it everywhere, so a top we
26250
+ // cannot identify is refused rather than trusted. That includes IE, which
26251
+ // has no ancestorOrigins at all: the alternative is a browser-shaped hole in
26252
+ // a control somebody opted into, which is worse than the frame running
26253
+ // standalone there.
26254
+ var enforcing = enforceAllowedOriginServers();
26255
+ var topOrigin = enforcing ? readTopOrigin(this._window) : null;
26256
+ if (!enforcing || (topOrigin && isTrustedOrigin(topOrigin))) {
26257
+ this._impl.connectToTop({
26258
+ action: 'pendo::connect',
26259
+ frameId: this._store.state.frames.frameId
26260
+ }, topOrigin || this._targetOrigin);
26261
+ }
26262
+ else {
26263
+ // nothing reads frames/connectFailed, so without this the frame just
26264
+ // goes quiet after ~3 minutes of retries with nothing to point at
26265
+ log.warn('cross-frame connect refused: top origin is not allowed', {
26266
+ topOrigin: topOrigin || 'unknown'
26267
+ });
26268
+ }
26269
+ // when refused, fall through to the timeout so the frame still gives up
26270
+ // cleanly via setConnectFailed instead of hanging half-connected
25971
26271
  setTimeout$1(_.bind(function () {
25972
26272
  if (this._connected)
25973
26273
  return;
@@ -26213,23 +26513,6 @@ _.extend(PostMessageImpl.prototype, ImplCommon, {
26213
26513
  }
26214
26514
  });
26215
26515
 
26216
- function getLocationOrigin(window) {
26217
- const strNull = 'null';
26218
- const origin = window.location.origin;
26219
- if (origin === strNull && window.parent && window.parent != window) {
26220
- try {
26221
- const parentOrigin = window.parent.location.origin;
26222
- if (parentOrigin && parentOrigin !== strNull) {
26223
- return parentOrigin;
26224
- }
26225
- }
26226
- catch (e) {
26227
- // cross-origin ancestor; cannot read top location
26228
- }
26229
- }
26230
- return origin;
26231
- }
26232
-
26233
26516
  function createSingletonMessageHandler(window, isTrustedOriginFn) {
26234
26517
  var listeners = [];
26235
26518
  var secure = false;
@@ -29275,7 +29558,8 @@ const PluginAPI = {
29275
29558
  collectEvent,
29276
29559
  hasConsentForAnalyticsEvent,
29277
29560
  getTextCollectionMode,
29278
- isReplayConsentDenied
29561
+ isReplayConsentDenied,
29562
+ isUtmConsentDenied
29279
29563
  },
29280
29564
  attachEvent,
29281
29565
  attachEventInternal,
@@ -29325,6 +29609,7 @@ const PluginAPI = {
29325
29609
  q,
29326
29610
  SendQueue,
29327
29611
  sessionStorage: agentStorage.getSession(),
29612
+ storageIsDisabled,
29328
29613
  sizeElements,
29329
29614
  store,
29330
29615
  Tombstone,
@@ -29345,7 +29630,8 @@ const PluginAPI = {
29345
29630
  escapeRegExp,
29346
29631
  getZoneSafeMethod(method, target) {
29347
29632
  return getZoneSafeMethod$1(_, method, target);
29348
- }
29633
+ },
29634
+ isNativeCode
29349
29635
  },
29350
29636
  constants: {
29351
29637
  URL_MAX_LENGTH,
@@ -29478,16 +29764,18 @@ function initIdentityEvents() {
29478
29764
  }
29479
29765
  }
29480
29766
  };
29481
- const requestReidentification = function () {
29482
- if (!isTrackingConsentDenied() && isVisitorDNT()) {
29483
- Events.identifyRequested.trigger();
29767
+ const resumeIdentity = function (newIdentity) {
29768
+ if (!newIdentity || isTrackingConsentDenied() || !isVisitorDNT()) {
29769
+ return;
29484
29770
  }
29771
+ updateOptions(newIdentity);
29485
29772
  };
29486
29773
  const handleConsentChange = function (event) {
29487
29774
  var { identity } = event.data[0];
29775
+ var newIdentity = event.data[1];
29488
29776
  if (_.isUndefined(identity))
29489
29777
  return;
29490
- identity ? requestReidentification() : clearSession({ doNotTrack: true });
29778
+ identity ? resumeIdentity(newIdentity) : clearSession({ doNotTrack: true });
29491
29779
  };
29492
29780
  Events.identify.on(collectIdentifyEvent);
29493
29781
  Events.metadata.on(collectMetadataEvent);
@@ -29503,6 +29791,62 @@ function initIdentityEvents() {
29503
29791
  };
29504
29792
  }
29505
29793
 
29794
+ const CONSENT_TIERS = 'cookieConsentTiers';
29795
+ function applyCookieConsent(tiers, identity) {
29796
+ rememberConsentTiers(tiers);
29797
+ const enabledProducts = applyConsentTiers(tiers);
29798
+ Events['consent:changed'].trigger(enabledProducts, identity);
29799
+ log.debug('Cookie consent updated for ' + JSON.stringify(tiers) + '.');
29800
+ return enabledProducts;
29801
+ }
29802
+ function rememberConsentTiers(tiers) {
29803
+ const options = ConfigReader.getLocalConfig() || {};
29804
+ options[CONSENT_TIERS] = tiers;
29805
+ ConfigReader.setLocalConfig(options);
29806
+ }
29807
+ function getCookieConsentTiers() {
29808
+ return ConfigReader.get(CONSENT_TIERS);
29809
+ }
29810
+ function isCurrentCookieConsent(tiers) {
29811
+ return sameConsentTiers(tiers, getCookieConsentTiers());
29812
+ }
29813
+ /**
29814
+ * Enforces the cookie consent policy a visitor has agreed to. The policy is defined by the subscription's cookie consent configuration,
29815
+ * and is used to determine whether each product the web SDK may run.
29816
+ *
29817
+ * A visitor who was not being tracked has no identity for the web SDK to resume, so pass one in
29818
+ * `identity` when the new tiers start allowing identity. It is ignored otherwise.
29819
+ *
29820
+ * @access public
29821
+ * @category Core
29822
+ * @param {string | string[]} tiers tier or tiers the visitor consented to
29823
+ * @param {object} [identity] visitor and account to identify, in the same shape as
29824
+ * `pendo.updateOptions` -- applied only when consent moves a do-not-track visitor to a tier
29825
+ * that permits identity
29826
+ * @returns {object} map of product to its resolved value -- `guides` and `identity` are
29827
+ * booleans (identity disabled means DNT), `textCollection` is `none`/`whitelist`/`all`,
29828
+ * `analytics` and `guideAnalytics` are each an array of allowed event types
29829
+ * @example
29830
+ * pendo.updateCookieConsent('strictlyNecessary');
29831
+ * @example
29832
+ * pendo.updateCookieConsent(['strictlyNecessary', 'functional']);
29833
+ * @example
29834
+ * pendo.updateCookieConsent(['strictlyNecessary', 'functional'], {
29835
+ * visitor: { id: 'put_visitor_id_here', role: 'admin' },
29836
+ * account: { id: 'put_account_id_here' }
29837
+ * });
29838
+ */
29839
+ function updateCookieConsent(tiers, identity) {
29840
+ store.dispatch('frames/changeCookieConsent', { tiers });
29841
+ return applyCookieConsent(tiers, identity);
29842
+ }
29843
+ function initializeCookieConsent(tiers) {
29844
+ resetConsentPolicy();
29845
+ if (tiers) {
29846
+ applyCookieConsent(tiers);
29847
+ }
29848
+ }
29849
+
29506
29850
  /*
29507
29851
  * ResourceCenterActivity Event
29508
29852
  *
@@ -29986,6 +30330,8 @@ function initialize(options) {
29986
30330
  }
29987
30331
  // Save the options somewhere
29988
30332
  ConfigReader.setLocalConfig(options);
30333
+ const cookieConsentTiers = _.get(options, 'cookieConsentTiers');
30334
+ initializeCookieConsent(cookieConsentTiers);
29989
30335
  initDataHost();
29990
30336
  exportDataHost(pendo$1);
29991
30337
  const cookieDomain = ConfigReader.get('cookieDomain');
@@ -30177,8 +30523,9 @@ function autoInitialize() {
30177
30523
  log.info('already running');
30178
30524
  return;
30179
30525
  }
30180
- if (window.pendo_options) { // Support pre-2.0 initialization
30181
- initialize(window.pendo_options);
30526
+ const snippetOptions = getLegacyGlobalSnippetOptions();
30527
+ if (snippetOptions) { // Support pre-2.0 initialization
30528
+ initialize(snippetOptions);
30182
30529
  }
30183
30530
  flushCallQueue();
30184
30531
  }
@@ -31232,28 +31579,6 @@ logPublic.setActiveContexts = setActiveContexts;
31232
31579
  logPublic.showLogHistory = showLogHistory;
31233
31580
  logPublic.getLoggedContexts = getLoggedContexts;
31234
31581
 
31235
- /**
31236
- * Enforces the cookie consent policy a visitor has agreed to. The policy is defined by the subscription's cookie consent configuration,
31237
- * and is used to determine whether each product the web SDK may run.
31238
- *
31239
- * @access public
31240
- * @category Core
31241
- * @param {string | string[]} tiers tier or tiers the visitor consented to
31242
- * @returns {object} map of product to its resolved value -- `guides` and `identity` are
31243
- * booleans (identity disabled means DNT), `textCollection` is `none`/`whitelist`/`all`,
31244
- * `analytics` and `guideAnalytics` are each an array of allowed event types
31245
- * @example
31246
- * pendo.updateCookieConsent('strictlyNecessary');
31247
- * @example
31248
- * pendo.updateCookieConsent(['strictlyNecessary', 'functional']);
31249
- */
31250
- function updateCookieConsent(tiers) {
31251
- const enabledProducts = applyConsentTiers(tiers);
31252
- Events['consent:changed'].trigger(enabledProducts);
31253
- log.debug('Cookie consent updated for ' + JSON.stringify(tiers) + '.');
31254
- return enabledProducts;
31255
- }
31256
-
31257
31582
  /**
31258
31583
  * Shared helper for event collection
31259
31584
  */
@@ -34051,20 +34376,6 @@ function GuideStep(guide) {
34051
34376
  stepId: step.id,
34052
34377
  isShown: step.isShown()
34053
34378
  });
34054
- // Android and some other mobile frameworks (Cordova webviews) send a resize event when the keyboard opens on focusing a text input
34055
- // due to redrawing the guide on resize this ends up closing the keyboard immediately.
34056
- // We need to ignore resize events in these cases to not redraw yet
34057
- // This idea comes from the 2nd solution of: https://stackoverflow.com/questions/38296574/disappearing-soft-keyboard-in-android-app
34058
- if (step.guideElement && step.guideElement[0]) {
34059
- step.attachEvent(step.guideElement[0], 'focusin', function (e) {
34060
- if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
34061
- pendo$1.ignoreResize = true;
34062
- setTimeout$1(function () {
34063
- pendo$1.ignoreResize = false;
34064
- }, 1000);
34065
- }
34066
- });
34067
- }
34068
34379
  };
34069
34380
  /**
34070
34381
  * Hides the Step if shown. Use an argument of an Object containing a
@@ -34291,7 +34602,7 @@ const ErrorLogModule = (function () {
34291
34602
  }
34292
34603
  const errMsg = `[${logEvent}]`;
34293
34604
  if (logEvent.level === LogLevel.Critical) {
34294
- const options = window.pendo_options || {};
34605
+ const options = getLegacyGlobalSnippetOptions() || {};
34295
34606
  logError(errMsg);
34296
34607
  if (pendo$1.logStackTraces !== false && options.logStackTraces !== false && error.stack) {
34297
34608
  writeErrorPOST(`${errMsg}\n${error.stack}`);
@@ -35029,6 +35340,22 @@ var FramesModule = (function () {
35029
35340
  return;
35030
35341
  identifyVisitor(payload.visitorId, payload.accountId);
35031
35342
  },
35343
+ changeCookieConsent(context, payload) {
35344
+ crossFrameChannel.postMessage({
35345
+ action: 'frames/receiveCookieConsent',
35346
+ frameId: context.state.frameId,
35347
+ tiers: payload.tiers
35348
+ });
35349
+ },
35350
+ receiveCookieConsent(context, payload) {
35351
+ if (context.getters.getMyApiKey() !== payload.apiKey)
35352
+ return;
35353
+ if (context.state.frameId === payload.frameId)
35354
+ return;
35355
+ if (isCurrentCookieConsent(payload.tiers))
35356
+ return;
35357
+ applyCookieConsent(payload.tiers);
35358
+ },
35032
35359
  setReady(context, payload) {
35033
35360
  context.commit('setReady', payload.isReady);
35034
35361
  },
@@ -39010,6 +39337,7 @@ const IFrameMonitor = (function () {
39010
39337
  let pendoGlobal;
39011
39338
  let pluginAPI;
39012
39339
  let topDownSync;
39340
+ let identitySync;
39013
39341
  return {
39014
39342
  name: 'IFrameMonitor',
39015
39343
  initialize: init,
@@ -39025,7 +39353,12 @@ const IFrameMonitor = (function () {
39025
39353
  removeHandlers();
39026
39354
  }
39027
39355
  function onChildFrameJoined() {
39028
- syncIdentity();
39356
+ // Consent first. A frame applies these in the order they arrive, and one whose policy
39357
+ // still denies tracking will throw away the identity sent below.
39358
+ syncCookieConsent();
39359
+ if (identitySync) {
39360
+ syncIdentity();
39361
+ }
39029
39362
  }
39030
39363
  function getAgentUrl() {
39031
39364
  const selfHostedUrl = pluginAPI.ConfigReader.get('selfHostedAgentUrl');
@@ -39128,14 +39461,23 @@ const IFrameMonitor = (function () {
39128
39461
  return;
39129
39462
  pluginAPI.store.dispatch('frames/syncIdentity', { visitorId, accountId });
39130
39463
  }
39464
+ function syncCookieConsent() {
39465
+ const tiers = getCookieConsentTiers();
39466
+ if (!tiers)
39467
+ return; // nothing to replay for a subscription that never configured consent
39468
+ pluginAPI.store.dispatch('frames/changeCookieConsent', { tiers });
39469
+ }
39131
39470
  function monitorFrames() {
39132
39471
  if (pluginAPI.ConfigReader.get('autoFrameInstall')) {
39133
39472
  pluginAPI.Events.ready.on(readyHandler);
39134
39473
  }
39135
- if (pluginAPI.ConfigReader.get('frameIdentitySync')) {
39474
+ identitySync = pluginAPI.ConfigReader.get('frameIdentitySync');
39475
+ // A frame that installs itself misses consent changes made before it joined, whether or
39476
+ // not identity is being shared with it.
39477
+ pluginAPI.Events.childFrameJoined.on(onChildFrameJoined);
39478
+ if (identitySync) {
39136
39479
  pluginAPI.Events.metadata.on(metadataHandler);
39137
39480
  pluginAPI.Events.identify.on(identifyHandler);
39138
- pluginAPI.Events.childFrameJoined.on(onChildFrameJoined);
39139
39481
  }
39140
39482
  }
39141
39483
  function removeHandlers() {
@@ -42300,7 +42642,8 @@ class WebAnalytics {
42300
42642
  const { bind } = pendo._;
42301
42643
  this.subscriptions = [
42302
42644
  PluginAPI.attachEvent(PluginAPI.Events, 'eventCaptured', bind(this.addUtmToEvent, this)),
42303
- PluginAPI.attachEvent(PluginAPI.Events, 'sessionChanged', bind(this.sessionChanged, this))
42645
+ PluginAPI.attachEvent(PluginAPI.Events, 'sessionChanged', bind(this.sessionChanged, this)),
42646
+ PluginAPI.attachEvent(PluginAPI.Events, 'consent:changed', bind(this.handleConsentChange, this))
42304
42647
  ];
42305
42648
  this.suffix = this.api.ConfigReader.get('identityStorageSuffix');
42306
42649
  /**
@@ -42312,11 +42655,19 @@ class WebAnalytics {
42312
42655
  * @label WEB_ANALYTICS_STORAGE_KEY
42313
42656
  */
42314
42657
  this.api.agentStorage.registry.addLocal(WEB_ANALYTICS_STORAGE_KEY, { cookieSuffix: this.suffix, isSecure: true });
42315
- let params = this.loadParameters(PluginAPI.agentStorage);
42316
- if (!params || pendo._.isEmpty(params)) {
42658
+ if (PluginAPI.analytics.isUtmConsentDenied()) {
42659
+ return this.clearParameters(PluginAPI.agentStorage);
42660
+ }
42661
+ this.captureParameters();
42662
+ }
42663
+ captureParameters() {
42664
+ if (this.api.analytics.isUtmConsentDenied())
42665
+ return;
42666
+ let params = this.loadParameters(this.api.agentStorage);
42667
+ if (!params || this.pendo._.isEmpty(params)) {
42317
42668
  params = this.extractParameters(window.location.href, document.referrer);
42318
42669
  }
42319
- this.storeParameters(params, PluginAPI.agentStorage);
42670
+ this.storeParameters(params, this.api.agentStorage);
42320
42671
  }
42321
42672
  extractParameters(url, referrer) {
42322
42673
  const locationUrl = new URL(url);
@@ -42372,10 +42723,16 @@ class WebAnalytics {
42372
42723
  }
42373
42724
  }
42374
42725
  storeParameters(params, storage) {
42726
+ if (this.api.analytics.isUtmConsentDenied())
42727
+ return;
42375
42728
  if (this.pendo._.size(params) > 0) {
42376
42729
  storage.write(WEB_ANALYTICS_STORAGE_KEY, JSON.stringify(params));
42377
42730
  }
42378
42731
  }
42732
+ clearParameters(storage) {
42733
+ storage.clear(WEB_ANALYTICS_STORAGE_KEY);
42734
+ storage.clear(WEB_ANALYTICS_STORAGE_KEY, false, '');
42735
+ }
42379
42736
  loadParameters(storage) {
42380
42737
  const storedParamsJson = storage.read(WEB_ANALYTICS_STORAGE_KEY) ||
42381
42738
  storage.read(WEB_ANALYTICS_STORAGE_KEY, false, '');
@@ -42390,6 +42747,8 @@ class WebAnalytics {
42390
42747
  this.subscriptions.length = 0;
42391
42748
  }
42392
42749
  addUtmToEvent(event) {
42750
+ if (this.api.analytics.isUtmConsentDenied())
42751
+ return;
42393
42752
  let utm;
42394
42753
  try {
42395
42754
  utm = this.loadParameters(this.api.agentStorage);
@@ -42403,11 +42762,19 @@ class WebAnalytics {
42403
42762
  }
42404
42763
  sessionChanged() {
42405
42764
  const agentStorage = this.api.agentStorage;
42406
- agentStorage.clear(WEB_ANALYTICS_STORAGE_KEY);
42407
- agentStorage.clear(WEB_ANALYTICS_STORAGE_KEY, false, '');
42765
+ this.clearParameters(agentStorage);
42408
42766
  const params = this.extractParameters(window.location.href, document.referrer);
42409
42767
  this.storeParameters(params, agentStorage);
42410
42768
  }
42769
+ handleConsentChange(event) {
42770
+ const { utm } = event.data[0];
42771
+ if (this.pendo._.isUndefined(utm))
42772
+ return;
42773
+ if (this.api.analytics.isUtmConsentDenied()) {
42774
+ return this.clearParameters(this.api.agentStorage);
42775
+ }
42776
+ this.captureParameters();
42777
+ }
42411
42778
  }
42412
42779
  var WebAnalytics$1 = new WebAnalytics();
42413
42780
 
@@ -42735,6 +43102,310 @@ var guideMarkdownUtil = {
42735
43102
  removeMarkdownSyntax
42736
43103
  };
42737
43104
 
43105
+ const AUTO_ADVANCE_DELAY_MS = 250;
43106
+ function isAutoAdvanceRequested(step, _) {
43107
+ return _.get(step, 'attributes.autoAdvance') === true;
43108
+ }
43109
+ // Survey-authored DOM carries no question-type classes, so eligibility keys off
43110
+ // answer-control shape: every required poll id must be radio-backed, which also
43111
+ // refuses a future non-radio type by default.
43112
+ function isStepEligibleInDom(step, _, requiredPollIds) {
43113
+ const guideElement = step && step.guideElement;
43114
+ if (!guideElement)
43115
+ return false;
43116
+ if (guideElement.find('textarea, input:text, input:checkbox, select').length)
43117
+ return false;
43118
+ const pollIds = _.uniq(_.map(guideElement.find('[data-pendo-poll-id]'), (element) => element.getAttribute('data-pendo-poll-id')));
43119
+ if (!pollIds.length)
43120
+ return false;
43121
+ return _.all(pollIds, (pollId) => _.contains(requiredPollIds, pollId) &&
43122
+ guideElement.find(`input:radio[data-pendo-poll-id="${pollId}"]`).length > 0);
43123
+ }
43124
+ function hideSubmitButtons(step, _, submitButtons) {
43125
+ const hiddenButtons = [];
43126
+ _.each(submitButtons, (button) => {
43127
+ const element = step.guideElement.find(`#${button.props.id}`)[0];
43128
+ if (!element)
43129
+ return;
43130
+ // display, not visibility: a reserved-but-invisible box leaves an empty gap
43131
+ // at the foot of the step. Authored styles reach the button inline
43132
+ // (build.js:151), so the original value is kept rather than blanked.
43133
+ hiddenButtons.push({ element, display: element.style.display });
43134
+ element.style.display = 'none';
43135
+ });
43136
+ return function restoreSubmitButtons() {
43137
+ _.each(hiddenButtons, (hiddenButton) => {
43138
+ hiddenButton.element.style.display = hiddenButton.display;
43139
+ });
43140
+ };
43141
+ }
43142
+ // The container's pixel height is measured during render (layout.js) before custom
43143
+ // scripts run, so collapsing a row afterwards leaves the box stale and the step keeps
43144
+ // the empty space. Same reflow pair metadata-substitution uses, scoped to this step so
43145
+ // a second guide on screen is left alone.
43146
+ function reflowGuide(step, _, pendo) {
43147
+ const recalculateGuideHeight = _.get(pendo, 'BuildingBlocks.BuildingBlockGuides.recalculateGuideHeight');
43148
+ try {
43149
+ if (_.isFunction(pendo.flexElement)) {
43150
+ pendo.flexElement(step.guideElement.find(guideMarkdownUtil.containerSelector));
43151
+ }
43152
+ if (_.isFunction(recalculateGuideHeight)) {
43153
+ recalculateGuideHeight(`pendo-g-${step.id}`, step.guideElement);
43154
+ }
43155
+ }
43156
+ catch (error) {
43157
+ pendo.log('[auto-advance] guide reflow failed, the step may keep an empty row', error && error.message);
43158
+ }
43159
+ }
43160
+ function createInertController() {
43161
+ return { isActive: false, notifyAnswered() { }, cancel() { } };
43162
+ }
43163
+ function createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds) {
43164
+ const _ = pendo._;
43165
+ const inert = createInertController();
43166
+ if (!isAutoAdvanceRequested(step, _))
43167
+ return inert;
43168
+ // Only reachable for API-authored guides; choosing between two
43169
+ // submitPollAndGoToStep targets would be a guess.
43170
+ if (_.size(submitButtons) !== 1) {
43171
+ pendo.log(`[auto-advance] not arming: step ${step.id} needs exactly one submit button, found ${_.size(submitButtons)}`);
43172
+ return inert;
43173
+ }
43174
+ if (!isStepEligibleInDom(step, _, requiredPollIds)) {
43175
+ pendo.log(`[auto-advance] not arming: rendered step ${step.id} failed the eligibility re-check`);
43176
+ return inert;
43177
+ }
43178
+ const submitButton = submitButtons[0];
43179
+ // A click binds every action on the node and dispatches one event each
43180
+ // (build.js:205-207); we dispatch one, so a second action would be dropped.
43181
+ if (_.size(submitButton.actions) !== 1) {
43182
+ pendo.log(`[auto-advance] not arming: step ${step.id} submit button carries ${_.size(submitButton.actions)} actions, expected one`);
43183
+ return inert;
43184
+ }
43185
+ const submitAction = _.find(submitButton.actions, (action) => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep');
43186
+ if (!submitAction)
43187
+ return inert;
43188
+ let hasFired = false;
43189
+ let restoreSubmitButtons = _.noop;
43190
+ function fire() {
43191
+ if (hasFired)
43192
+ return;
43193
+ hasFired = true;
43194
+ try {
43195
+ // Shape-identical to what bindActionToNode emits for a click, so
43196
+ // beforeAdvance consumers intercept an auto-advance the same way.
43197
+ step.eventRouter.eventable.trigger('pendoEvent', {
43198
+ action: submitAction.action,
43199
+ params: submitAction.parameters,
43200
+ step,
43201
+ ignore: !!submitAction.ignore,
43202
+ srcElement: step.guideElement.find(`#${submitButton.props.id}`)[0]
43203
+ });
43204
+ }
43205
+ catch (error) {
43206
+ restoreSubmitButtons();
43207
+ reflowGuide(step, _, pendo);
43208
+ pendo.log('[auto-advance] submit dispatch failed, restored the submit button', error && error.message);
43209
+ }
43210
+ }
43211
+ const debouncedFire = _.debounce(fire, AUTO_ADVANCE_DELAY_MS);
43212
+ // Last, so nothing between here and the return can throw and leave the step
43213
+ // with a hidden button and no controller to un-hide it.
43214
+ restoreSubmitButtons = hideSubmitButtons(step, _, submitButtons);
43215
+ reflowGuide(step, _, pendo);
43216
+ return {
43217
+ isActive: true,
43218
+ notifyAnswered: debouncedFire,
43219
+ cancel: debouncedFire.cancel
43220
+ };
43221
+ }
43222
+
43223
+ const SURVEY_ANSWERS_KEY = 'pendo_survey_answers';
43224
+ const ANSWER_CONTROLS = 'input:radio, input:text, textarea, select';
43225
+ const TEXT_CONTROLS = 'input:text, textarea';
43226
+ // Matches MULTISTEP_CONTINUATION_TIME_LIMIT (walkthrough-guide.js:67), past which a guide is not continued.
43227
+ const ANSWER_TTL_MS = 12 * 60 * 60 * 1000;
43228
+ let pluginApi = null;
43229
+ let inMemoryAnswers = {};
43230
+ let persistedAnswersDropped = false;
43231
+ // A restricted storage.allowKeys silently rejects the session write, so mirror the visit here.
43232
+ const visitedSteps = {};
43233
+ function setPluginApi(api) {
43234
+ pluginApi = api;
43235
+ if (!api || !api.agentStorage)
43236
+ return;
43237
+ /**
43238
+ * Answers the visitor has given on each step of a survey, so a step re-shown
43239
+ * after Previous comes back filled in rather than blank.
43240
+ *
43241
+ * @name pendo_survey_answers
43242
+ * @category Cookies/sessionStorage
43243
+ * @access public
43244
+ * @label SURVEY_ANSWERS_KEY
43245
+ */
43246
+ api.agentStorage.registry.addSession(SURVEY_ANSWERS_KEY);
43247
+ }
43248
+ // getSession() checks the allow-list but not storageIsDisabled, which the cookie path applies.
43249
+ function persistenceAllowed() {
43250
+ const allowed = !!pluginApi &&
43251
+ !!pluginApi.sessionStorage &&
43252
+ typeof pluginApi.storageIsDisabled === 'function' &&
43253
+ !pluginApi.storageIsDisabled();
43254
+ if (allowed) {
43255
+ persistedAnswersDropped = false;
43256
+ }
43257
+ else {
43258
+ dropPersistedAnswers();
43259
+ }
43260
+ return allowed;
43261
+ }
43262
+ // agentStorage.clearAll only walks local keys, so a session key has to be dropped here.
43263
+ function dropPersistedAnswers() {
43264
+ if (persistedAnswersDropped || !pluginApi || !pluginApi.sessionStorage)
43265
+ return;
43266
+ persistedAnswersDropped = true;
43267
+ pluginApi.sessionStorage.removeItem(SURVEY_ANSWERS_KEY);
43268
+ }
43269
+ function readAll() {
43270
+ if (!persistenceAllowed())
43271
+ return inMemoryAnswers;
43272
+ try {
43273
+ return JSON.parse(pluginApi.sessionStorage.getItem(SURVEY_ANSWERS_KEY)) || {};
43274
+ }
43275
+ catch (error) {
43276
+ return {};
43277
+ }
43278
+ }
43279
+ function writeAll(answers) {
43280
+ if (!persistenceAllowed()) {
43281
+ inMemoryAnswers = answers;
43282
+ return;
43283
+ }
43284
+ pluginApi.sessionStorage.setItem(SURVEY_ANSWERS_KEY, JSON.stringify(answers));
43285
+ }
43286
+ // identify() sets the new id before the reload tears the step down, so resolve this once per render.
43287
+ function visitorKey(pendo) {
43288
+ return (pendo._.isFunction(pendo.get_visitor_id) && pendo.get_visitor_id()) || 'unknown';
43289
+ }
43290
+ function stateKey(step, guide, visitor) {
43291
+ return `${visitor}:${guide.id}:${step.id}`;
43292
+ }
43293
+ // Host pages reassign Date.now to return a Date, which JSON writes out as a string.
43294
+ function getNow() {
43295
+ return new Date().getTime();
43296
+ }
43297
+ function isFresh(writtenAt) {
43298
+ return !!writtenAt && (getNow() - writtenAt) <= ANSWER_TTL_MS;
43299
+ }
43300
+ function readRecord(step, guide, visitor) {
43301
+ const forVisitor = readAll()[visitor];
43302
+ const forGuide = forVisitor && forVisitor[guide.id];
43303
+ const record = (forGuide && forGuide[step.id]) || null;
43304
+ return isFresh(record && record.at) ? record : null;
43305
+ }
43306
+ function updateRecord(step, guide, visitor, changes) {
43307
+ const all = readAll();
43308
+ const current = readRecord(step, guide, visitor) || {};
43309
+ if (!all[visitor])
43310
+ all[visitor] = {};
43311
+ if (!all[visitor][guide.id])
43312
+ all[visitor][guide.id] = {};
43313
+ all[visitor][guide.id][step.id] = {
43314
+ at: getNow(),
43315
+ visited: changes.visited || current.visited || 0,
43316
+ answers: changes.answers || current.answers || {}
43317
+ };
43318
+ writeAll(all);
43319
+ }
43320
+ // Previous only renders after an auto-advance step, so any other guide needs none of this.
43321
+ function guideAutoAdvances(guide, _) {
43322
+ return _.any(_.get(guide, 'steps', []), (step) => isAutoAdvanceRequested(step, _));
43323
+ }
43324
+ function pollIdFor(control) {
43325
+ let node = control;
43326
+ while (node && node.getAttribute) {
43327
+ const pollId = node.getAttribute('data-pendo-poll-id');
43328
+ if (pollId)
43329
+ return pollId;
43330
+ node = node.parentNode;
43331
+ }
43332
+ return null;
43333
+ }
43334
+ // submitPoll never reads a checkbox back (event-router.js:264), so restoring one shows nothing sent.
43335
+ function isRadio(control) {
43336
+ return control.type === 'radio';
43337
+ }
43338
+ function collectAnswers(step, pendo) {
43339
+ const answers = {};
43340
+ pendo._.each(step.guideElement.find(ANSWER_CONTROLS), (control) => {
43341
+ const pollId = pollIdFor(control);
43342
+ // One value per poll id, first in document order, as submitPoll reads it.
43343
+ if (!pollId || answers[pollId])
43344
+ return;
43345
+ if (isRadio(control) && !control.checked)
43346
+ return;
43347
+ if (!control.value)
43348
+ return;
43349
+ answers[pollId] = [control.value];
43350
+ });
43351
+ return answers;
43352
+ }
43353
+ // No event dispatched: a restored answer must not reach the gate listeners or re-arm auto-advance.
43354
+ function restoreAnswers(step, pendo, answers) {
43355
+ pendo._.each(step.guideElement.find(ANSWER_CONTROLS), (control) => {
43356
+ const pollId = pollIdFor(control);
43357
+ const values = pollId && answers[pollId];
43358
+ if (!values)
43359
+ return;
43360
+ if (isRadio(control)) {
43361
+ if (pendo._.contains(values, control.value))
43362
+ control.checked = true;
43363
+ return;
43364
+ }
43365
+ control.value = values[0];
43366
+ });
43367
+ }
43368
+ function isRevisit(step, guide, pendo) {
43369
+ if (!guideAutoAdvances(guide, pendo._))
43370
+ return false;
43371
+ const visitor = visitorKey(pendo);
43372
+ if (isFresh(visitedSteps[stateKey(step, guide, visitor)]))
43373
+ return true;
43374
+ const record = readRecord(step, guide, visitor);
43375
+ return !!record && (!!record.visited || !pendo._.isEmpty(record.answers));
43376
+ }
43377
+ const AnswerCache = {
43378
+ name: 'AnswerCache',
43379
+ script(step, guide, pendo) {
43380
+ const visitor = visitorKey(pendo);
43381
+ const record = readRecord(step, guide, visitor);
43382
+ restoreAnswers(step, pendo, (record && record.answers) || {});
43383
+ // Marked on leaving, not on render: sessionStorage survives a reload, which is not a revisit.
43384
+ this.on('beforeUnmount', (event) => {
43385
+ if (event && event.reason === 'hidden')
43386
+ return;
43387
+ visitedSteps[stateKey(step, guide, visitor)] = getNow();
43388
+ updateRecord(step, guide, visitor, { visited: 1 });
43389
+ });
43390
+ const stepRoot = step.guideElement[0];
43391
+ // change does not bubble on legacy browsers; it is here only for keyboard selection.
43392
+ step.attachEvent(stepRoot, 'click', capture);
43393
+ step.attachEvent(stepRoot, 'change', capture);
43394
+ pendo._.each(step.guideElement.find(TEXT_CONTROLS), (control) => {
43395
+ step.attachEvent(control, 'input', capture);
43396
+ });
43397
+ function capture() {
43398
+ updateRecord(step, guide, visitor, { answers: collectAnswers(step, pendo) });
43399
+ }
43400
+ },
43401
+ test(step, guide, pendo) {
43402
+ const guideElement = step && step.guideElement;
43403
+ return !!guideElement &&
43404
+ guideAutoAdvances(guide, pendo._) &&
43405
+ guideElement.find(ANSWER_CONTROLS).length > 0;
43406
+ }
43407
+ };
43408
+
42738
43409
  function getZoneSafeMethod(_, method, target = window) {
42739
43410
  var zoneSymbol = '__symbol__';
42740
43411
  /* global Zone */
@@ -43211,124 +43882,6 @@ function substitutionIcon(color, size) {
43211
43882
  13.7239 15.5 14V17Z" fill="${color}"></path> </g></svg></div>`);
43212
43883
  }
43213
43884
 
43214
- const AUTO_ADVANCE_DELAY_MS = 250;
43215
- function isAutoAdvanceRequested(step, _) {
43216
- return _.get(step, 'attributes.autoAdvance') === true;
43217
- }
43218
- // Survey-authored DOM carries no question-type classes, so eligibility keys off
43219
- // answer-control shape: every required poll id must be radio-backed, which also
43220
- // refuses a future non-radio type by default.
43221
- function isStepEligibleInDom(step, _, requiredPollIds) {
43222
- const guideElement = step && step.guideElement;
43223
- if (!guideElement)
43224
- return false;
43225
- if (guideElement.find('textarea, input:text, input:checkbox, select').length)
43226
- return false;
43227
- const pollIds = _.uniq(_.map(guideElement.find('[data-pendo-poll-id]'), (element) => element.getAttribute('data-pendo-poll-id')));
43228
- if (!pollIds.length)
43229
- return false;
43230
- return _.all(pollIds, (pollId) => _.contains(requiredPollIds, pollId) &&
43231
- guideElement.find(`input:radio[data-pendo-poll-id="${pollId}"]`).length > 0);
43232
- }
43233
- function hideSubmitButtons(step, _, submitButtons) {
43234
- const hiddenButtons = [];
43235
- _.each(submitButtons, (button) => {
43236
- const element = step.guideElement.find(`#${button.props.id}`)[0];
43237
- if (!element)
43238
- return;
43239
- // display, not visibility: a reserved-but-invisible box leaves an empty gap
43240
- // at the foot of the step. Authored styles reach the button inline
43241
- // (build.js:151), so the original value is kept rather than blanked.
43242
- hiddenButtons.push({ element, display: element.style.display });
43243
- element.style.display = 'none';
43244
- });
43245
- return function restoreSubmitButtons() {
43246
- _.each(hiddenButtons, (hiddenButton) => {
43247
- hiddenButton.element.style.display = hiddenButton.display;
43248
- });
43249
- };
43250
- }
43251
- // The container's pixel height is measured during render (layout.js) before custom
43252
- // scripts run, so collapsing a row afterwards leaves the box stale and the step keeps
43253
- // the empty space. Same reflow pair metadata-substitution uses, scoped to this step so
43254
- // a second guide on screen is left alone.
43255
- function reflowGuide(step, _, pendo) {
43256
- const recalculateGuideHeight = _.get(pendo, 'BuildingBlocks.BuildingBlockGuides.recalculateGuideHeight');
43257
- try {
43258
- if (_.isFunction(pendo.flexElement)) {
43259
- pendo.flexElement(step.guideElement.find(guideMarkdownUtil.containerSelector));
43260
- }
43261
- if (_.isFunction(recalculateGuideHeight)) {
43262
- recalculateGuideHeight(`pendo-g-${step.id}`, step.guideElement);
43263
- }
43264
- }
43265
- catch (error) {
43266
- pendo.log('[auto-advance] guide reflow failed, the step may keep an empty row', error && error.message);
43267
- }
43268
- }
43269
- function createInertController() {
43270
- return { isActive: false, notifyAnswered() { }, cancel() { } };
43271
- }
43272
- function createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds) {
43273
- const _ = pendo._;
43274
- const inert = createInertController();
43275
- if (!isAutoAdvanceRequested(step, _))
43276
- return inert;
43277
- // Only reachable for API-authored guides; choosing between two
43278
- // submitPollAndGoToStep targets would be a guess.
43279
- if (_.size(submitButtons) !== 1) {
43280
- pendo.log(`[auto-advance] not arming: step ${step.id} needs exactly one submit button, found ${_.size(submitButtons)}`);
43281
- return inert;
43282
- }
43283
- if (!isStepEligibleInDom(step, _, requiredPollIds)) {
43284
- pendo.log(`[auto-advance] not arming: rendered step ${step.id} failed the eligibility re-check`);
43285
- return inert;
43286
- }
43287
- const submitButton = submitButtons[0];
43288
- // A click binds every action on the node and dispatches one event each
43289
- // (build.js:205-207); we dispatch one, so a second action would be dropped.
43290
- if (_.size(submitButton.actions) !== 1) {
43291
- pendo.log(`[auto-advance] not arming: step ${step.id} submit button carries ${_.size(submitButton.actions)} actions, expected one`);
43292
- return inert;
43293
- }
43294
- const submitAction = _.find(submitButton.actions, (action) => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep');
43295
- if (!submitAction)
43296
- return inert;
43297
- let hasFired = false;
43298
- let restoreSubmitButtons = _.noop;
43299
- function fire() {
43300
- if (hasFired)
43301
- return;
43302
- hasFired = true;
43303
- try {
43304
- // Shape-identical to what bindActionToNode emits for a click, so
43305
- // beforeAdvance consumers intercept an auto-advance the same way.
43306
- step.eventRouter.eventable.trigger('pendoEvent', {
43307
- action: submitAction.action,
43308
- params: submitAction.parameters,
43309
- step,
43310
- ignore: !!submitAction.ignore,
43311
- srcElement: step.guideElement.find(`#${submitButton.props.id}`)[0]
43312
- });
43313
- }
43314
- catch (error) {
43315
- restoreSubmitButtons();
43316
- reflowGuide(step, _, pendo);
43317
- pendo.log('[auto-advance] submit dispatch failed, restored the submit button', error && error.message);
43318
- }
43319
- }
43320
- const debouncedFire = _.debounce(fire, AUTO_ADVANCE_DELAY_MS);
43321
- // Last, so nothing between here and the return can throw and leave the step
43322
- // with a hidden button and no controller to un-hide it.
43323
- restoreSubmitButtons = hideSubmitButtons(step, _, submitButtons);
43324
- reflowGuide(step, _, pendo);
43325
- return {
43326
- isActive: true,
43327
- notifyAnswered: debouncedFire,
43328
- cancel: debouncedFire.cancel
43329
- };
43330
- }
43331
-
43332
43885
  const requiredElement = '<span class="_pendo-required-indicator" style="color:red; font-style:italic;" title="Question is required"> *</span>';
43333
43886
  const requiredSyntax = '{required/}';
43334
43887
  const RequiredQuestions = {
@@ -43354,9 +43907,14 @@ const RequiredQuestions = {
43354
43907
  }
43355
43908
  });
43356
43909
  }
43910
+ // Restored answers reach no listener, and the controller is still inert here.
43911
+ const revisited = isRevisit(step, guide, pendo);
43912
+ if (revisited) {
43913
+ evaluateRequiredQuestions(requiredQuestions);
43914
+ }
43357
43915
  // Armed last, after the listeners above: runCustomScripts abandons the rest of
43358
43916
  // this script on a throw, and the submit button is already disabled by now.
43359
- if (isAutoAdvanceRequested(step, pendo._)) {
43917
+ if (isAutoAdvanceRequested(step, pendo._) && !revisited) {
43360
43918
  this.on('beforeUnmount', () => autoAdvance.cancel());
43361
43919
  try {
43362
43920
  autoAdvance = createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds);
@@ -43707,7 +44265,9 @@ function GuideMarkdown() {
43707
44265
  const markdownScriptsEnabled = configReader.get(GUIDEMARKDOWN_CONFIG);
43708
44266
  if (!markdownScriptsEnabled)
43709
44267
  return;
43710
- guideMarkdown = [PollBranching, MetadataSubstitution, RequiredQuestions, SkipToEligibleStep];
44268
+ setPluginApi(PluginAPI);
44269
+ // AnswerCache first: it restores the answers RequiredQuestions reads the gate against.
44270
+ guideMarkdown = [AnswerCache, PollBranching, MetadataSubstitution, RequiredQuestions, SkipToEligibleStep];
43711
44271
  if (!pendo.designer) {
43712
44272
  pendo._.each(guideMarkdown, function (script) {
43713
44273
  PluginAPI.GlobalRuntime.addGlobalScript(script);
@@ -51498,12 +52058,15 @@ class Transport {
51498
52058
  }
51499
52059
  }
51500
52060
 
51501
- const CATCHALL_TYPES = ['load', 'click', 'track'];
51502
52061
  const EVENT_KINDS = {
51503
- catchall: 'catchall',
51504
- devlog: 'devlog',
51505
- recording: 'recording'
51506
- };
52062
+ AGENTIC: 'agentic',
52063
+ CATCHALL: 'catchall',
52064
+ DEVLOG: 'devlog',
52065
+ RECORDING: 'recording'
52066
+ };
52067
+ const CATCHALL_TYPES = ['load', 'click', 'track', 'guideSeen'];
52068
+ const AGENTIC_TYPES = ["prompt", "agent_response", "user_reaction"];
52069
+ const CONSENT_EXEMPT_TYPES = ['guideSeen', EVENT_KINDS.DEVLOG];
51507
52070
  class LiveReplayClient {
51508
52071
  constructor(pendo, api) {
51509
52072
  this.pendo = pendo;
@@ -51511,6 +52074,7 @@ class LiveReplayClient {
51511
52074
  this.ws = null;
51512
52075
  this.sequence = 0;
51513
52076
  this.unsubscribeEventCaptured = null;
52077
+ this.disabled = false;
51514
52078
  }
51515
52079
  buildWsUrl() {
51516
52080
  const url = new URL$1(`${this.pendo.HOST}/data/live-replay/${this.pendo.apiKey}`);
@@ -51523,11 +52087,18 @@ class LiveReplayClient {
51523
52087
  return url.href;
51524
52088
  }
51525
52089
  start(visitorId, onOpen) {
51526
- if (this.ws && this.ws.readyState !== WebSocket.CLOSED)
52090
+ const wsIsOpen = this.ws && this.ws.readyState !== WebSocket.CLOSED;
52091
+ if (wsIsOpen || this.disabled)
51527
52092
  return;
51528
52093
  this.sequence = 0;
51529
52094
  this.unsubscribeEventCaptured = this.api.attachEvent(this.api.Events, 'eventCaptured', this.pendo._.bind(this.forwardEvent, this));
51530
52095
  this.ws = new WebSocket(this.buildWsUrl());
52096
+ if (!this.api.util.isNativeCode(this.ws.send)) {
52097
+ this.disabled = true;
52098
+ this.api.log.warn('Live replay disabled: WebSocket send is not native.');
52099
+ this.stop();
52100
+ return;
52101
+ }
51531
52102
  this.ws.onopen = this.pendo._.bind(this.handleOpen, this, visitorId, onOpen);
51532
52103
  const boundHandleClose = this.pendo._.bind(this.handleClose, this);
51533
52104
  this.ws.onclose = boundHandleClose;
@@ -51583,21 +52154,31 @@ class LiveReplayClient {
51583
52154
  catch (_a) { }
51584
52155
  }
51585
52156
  sendRecordingEvent(fields) {
51586
- this.send(Object.assign(Object.assign({}, fields), { kind: EVENT_KINDS.recording, sequence: this.sequence++ }));
52157
+ this.send(Object.assign(Object.assign({}, fields), { kind: EVENT_KINDS.RECORDING, sequence: this.sequence++ }));
52158
+ }
52159
+ hasConsentToForward(type) {
52160
+ if (this.pendo._.contains(CONSENT_EXEMPT_TYPES, type))
52161
+ return true;
52162
+ return this.api.analytics.hasConsentForAnalyticsEvent(type);
51587
52163
  }
51588
52164
  forwardEvent(event) {
51589
52165
  if (!event || !event.data || !event.data.length)
51590
52166
  return;
51591
52167
  const capturedEvent = event.data[0];
51592
52168
  const config = { event: capturedEvent };
52169
+ if (this.pendo._.contains(AGENTIC_TYPES, capturedEvent.type)) {
52170
+ config.kind = EVENT_KINDS.AGENTIC;
52171
+ }
51593
52172
  if (this.pendo._.contains(CATCHALL_TYPES, capturedEvent.type)) {
51594
- config.kind = EVENT_KINDS.catchall;
52173
+ config.kind = EVENT_KINDS.CATCHALL;
51595
52174
  }
51596
- if (capturedEvent.type === EVENT_KINDS.devlog) {
51597
- config.kind = EVENT_KINDS.devlog;
52175
+ if (capturedEvent.type === EVENT_KINDS.DEVLOG) {
52176
+ config.kind = EVENT_KINDS.DEVLOG;
51598
52177
  }
51599
52178
  if (!config.kind)
51600
52179
  return;
52180
+ if (!this.hasConsentToForward(capturedEvent.type))
52181
+ return;
51601
52182
  // other eventCaptured listeners still mutate capturedEvent in place; defer until they're done
51602
52183
  this.pendo._.defer(() => {
51603
52184
  this.send(config);
@@ -52017,6 +52598,7 @@ class SessionRecorder {
52017
52598
  this.logStopReason('CONSENT_DENIED');
52018
52599
  this.stop();
52019
52600
  }
52601
+ this.clearSessionInfo();
52020
52602
  }
52021
52603
  else if (!this.isRecording()) {
52022
52604
  this.start();
@@ -52181,7 +52763,6 @@ class SessionRecorder {
52181
52763
  var bind = this.pendo._.bind;
52182
52764
  this.visitorConfig = visitorConfig;
52183
52765
  this.clearOldSessionId();
52184
- this.liveReplayClient.onLiveFlag(visitorConfig.live, this.visitorId);
52185
52766
  this.sendQueue.start();
52186
52767
  this.interval = setInterval(() => {
52187
52768
  if (!this.sendQueue.failed()) {
@@ -52194,6 +52775,7 @@ class SessionRecorder {
52194
52775
  errorHandler: this.errorHandler
52195
52776
  }, config));
52196
52777
  this._refreshIds();
52778
+ this.liveReplayClient.onLiveFlag(visitorConfig.live, this.visitorId, () => this.snapshot());
52197
52779
  this.onRecordingStart();
52198
52780
  }
52199
52781
  catch (e) {