@pendo/agent 2.342.0 → 2.343.1

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.
@@ -4058,8 +4058,8 @@ let SERVER = '';
4058
4058
  let ASSET_HOST = '';
4059
4059
  let ASSET_PATH = '';
4060
4060
  let DESIGNER_SERVER = '';
4061
- let VERSION = '2.342.0_';
4062
- let PACKAGE_VERSION = '2.342.0';
4061
+ let VERSION = '2.343.1_';
4062
+ let PACKAGE_VERSION = '2.343.1';
4063
4063
  let LOADER = 'xhr';
4064
4064
  /* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
4065
4065
  /**
@@ -4476,15 +4476,35 @@ class LogEmitter extends EventTarget {
4476
4476
  }
4477
4477
  const log = new LogEmitter();
4478
4478
 
4479
- /** Key used to lookup the consent collection policy in the pendo configuration. */
4480
4479
  const CONSENT_COLLECTION = 'cookieConsentCollection';
4481
- /** Key used to lookup the analytics events in the consent collection policy. */
4482
4480
  const ANALYTICS = 'analytics';
4483
- /** Key used to lookup the guide analytics events in the consent collection policy. */
4484
4481
  const GUIDE_ANALYTICS = 'guideAnalytics';
4485
- /** Key used to lookup the text collection mode in the consent collection policy. */
4486
4482
  const TEXT_COLLECTION = 'textCollection';
4487
4483
  const TEXT_COLLECTION_LEVELS = ['none', 'whitelist', 'all'];
4484
+ const ANALYTICS_EVENT_TYPES = {
4485
+ agentResponse: 'agent_response',
4486
+ clicks: 'click',
4487
+ fieldChanges: 'change',
4488
+ focus: 'focus',
4489
+ formSubmits: 'submit',
4490
+ identify: 'identify',
4491
+ meta: 'meta',
4492
+ pageLoads: 'load',
4493
+ prompt: 'prompt',
4494
+ trackEvents: 'track',
4495
+ userReaction: 'user_reaction'
4496
+ };
4497
+ const GUIDE_EVENT_TYPES = [
4498
+ 'guideActivity',
4499
+ 'guideAdvanced',
4500
+ 'guideDismissed',
4501
+ 'guideNotSeen',
4502
+ 'guideSeen',
4503
+ 'guideSnoozeCanceled',
4504
+ 'guideSnoozed',
4505
+ 'guideTimeout',
4506
+ 'pollResponse'
4507
+ ];
4488
4508
  let enabledProducts = null;
4489
4509
  function getClassification() {
4490
4510
  return ConfigReader.get(CONSENT_COLLECTION) || {};
@@ -4504,30 +4524,59 @@ function warnOnUnconfiguredTiers(grantedTiers, classification) {
4504
4524
  function moreTextPermissive(a, b) {
4505
4525
  return TEXT_COLLECTION_LEVELS.indexOf(b) > TEXT_COLLECTION_LEVELS.indexOf(a) ? b : a;
4506
4526
  }
4507
- function orBoolean(a, b) {
4508
- return a || Boolean(b);
4527
+ function toTextCollectionLevel(mode) {
4528
+ return _.contains(TEXT_COLLECTION_LEVELS, mode) ? mode : TEXT_COLLECTION_LEVELS[0];
4509
4529
  }
4510
- function unionArrays(a, b) {
4511
- return _.union(a, b);
4530
+ function toAnalyticsEventTypes(eventCollection) {
4531
+ return _.reduce(ANALYTICS_EVENT_TYPES, function (types, eventType, key) {
4532
+ if (eventCollection[key] === true) {
4533
+ types.push(eventType);
4534
+ }
4535
+ return types;
4536
+ }, []);
4537
+ }
4538
+ function normalizeTier(tier) {
4539
+ const { visitorIdentity = {}, analytics = {}, guides = {}, sessionReplay = {} } = tier || {};
4540
+ const eventCollection = analytics.eventCollection || {};
4541
+ const analyticsEnabled = analytics.enabled === true;
4542
+ return {
4543
+ identity: visitorIdentity.trackVisitors === true,
4544
+ guides: guides.enabled === true,
4545
+ replay: sessionReplay.enabled === true,
4546
+ analytics: analyticsEnabled ? toAnalyticsEventTypes(eventCollection) : [],
4547
+ guideAnalytics: analyticsEnabled && eventCollection.guideServing === true ? GUIDE_EVENT_TYPES.slice() : [],
4548
+ textCollection: analyticsEnabled ? toTextCollectionLevel(eventCollection.textCollectionMode) : TEXT_COLLECTION_LEVELS[0]
4549
+ };
4550
+ }
4551
+ function deniedProducts() {
4552
+ return {
4553
+ identity: false,
4554
+ guides: false,
4555
+ replay: false,
4556
+ [ANALYTICS]: [],
4557
+ [GUIDE_ANALYTICS]: [],
4558
+ [TEXT_COLLECTION]: TEXT_COLLECTION_LEVELS[0]
4559
+ };
4560
+ }
4561
+ function combineTiers(resolved, granted) {
4562
+ return {
4563
+ identity: resolved.identity || granted.identity,
4564
+ guides: resolved.guides || granted.guides,
4565
+ replay: resolved.replay || granted.replay,
4566
+ [ANALYTICS]: _.union(resolved[ANALYTICS], granted[ANALYTICS]),
4567
+ [GUIDE_ANALYTICS]: _.union(resolved[GUIDE_ANALYTICS], granted[GUIDE_ANALYTICS]),
4568
+ [TEXT_COLLECTION]: moreTextPermissive(resolved[TEXT_COLLECTION], granted[TEXT_COLLECTION])
4569
+ };
4512
4570
  }
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
4571
  /**
4523
- * Resolves the granted tiers against the configured product classification.
4572
+ * Resolves the tiers a visitor consented to into what each product is allowed to do.
4573
+ *
4574
+ * Every product starts denied and only granted tiers contribute, so a tier the visitor
4575
+ * hasn't opted into has no say -- it can't deny anything, and it can't allow anything.
4524
4576
  *
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.
4577
+ * A subscription with no cookie consent configured resolves to an empty map instead, which
4578
+ * every read below treats as unconstrained: consent that was never set up can't deny
4579
+ * anything either.
4531
4580
  *
4532
4581
  * @access private
4533
4582
  * @param {string | string[]} tiers tier or tiers the visitor consented to
@@ -4536,26 +4585,22 @@ const DEFAULT_RESOLVER = { initial: false, combine: orBoolean };
4536
4585
  function applyConsentTiers(tiers) {
4537
4586
  const cookieConsentCollection = getClassification();
4538
4587
  const consentedCookieTiers = toTierList(tiers);
4539
- const resolved = {};
4540
4588
  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;
4589
+ if (_.isEmpty(cookieConsentCollection)) {
4590
+ enabledProducts = {};
4591
+ return getEnabledProducts();
4592
+ }
4593
+ enabledProducts = _.reduce(consentedCookieTiers, function (resolved, tier) {
4594
+ const granted = cookieConsentCollection[tier];
4595
+ return granted ? combineTiers(resolved, normalizeTier(granted)) : resolved;
4596
+ }, deniedProducts());
4551
4597
  return getEnabledProducts();
4552
4598
  }
4553
4599
  function getEnabledProducts() {
4554
4600
  return enabledProducts ? _.extend({}, enabledProducts) : null;
4555
4601
  }
4556
4602
  /**
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.
4603
+ * A product is enabled only if some granted tier turned it on or cookie consent has not been set.
4559
4604
  *
4560
4605
  * @access private
4561
4606
  * @param {string} product
@@ -4569,6 +4614,9 @@ function isProductEnabled(product) {
4569
4614
  function isTrackingConsentDenied() {
4570
4615
  return !isProductEnabled('identity');
4571
4616
  }
4617
+ function isGuideConsentDenied() {
4618
+ return !isProductEnabled('guides');
4619
+ }
4572
4620
  /**
4573
4621
  * Session replay is denied whenever tracking itself is denied (DNT) -- a visitor
4574
4622
  * who can't be identified shouldn't have their session recorded either -- or when a
@@ -4604,6 +4652,9 @@ function hasConsentForAnalyticsEvent(type) {
4604
4652
  function hasConsentForGuideEvent(type) {
4605
4653
  return hasConsentForProduct(type, GUIDE_ANALYTICS);
4606
4654
  }
4655
+ function resetConsentPolicy() {
4656
+ enabledProducts = null;
4657
+ }
4607
4658
 
4608
4659
  const allowKeys = function () {
4609
4660
  var allowKeys = ConfigReader.get('storage.allowKeys') || '*';
@@ -22161,14 +22212,29 @@ function ClickActivation() {
22161
22212
  }
22162
22213
  stopEvent(evt);
22163
22214
  };
22215
+ // Safari does not focus the badge on mousedown, so focus leaves the app instead.
22216
+ // Apps that re-render on blur can remove the badge before mouseup, and the browser
22217
+ // then generates no click at all. Holding focus where it is avoids that (APP-168412)
22218
+ var preserveFocus = function (evt) {
22219
+ if (evt.preventDefault) {
22220
+ evt.preventDefault();
22221
+ }
22222
+ else {
22223
+ evt.returnValue = false;
22224
+ }
22225
+ };
22164
22226
  badge.after('show', function () {
22165
22227
  if (!attached) {
22166
22228
  attachEvent(badgeElem, 'click', click);
22229
+ if (badge.preserveFocusOnClick) {
22230
+ attachEvent(badgeElem, 'mousedown', preserveFocus);
22231
+ }
22167
22232
  attached = true;
22168
22233
  }
22169
22234
  });
22170
22235
  badge.after('hide', function () {
22171
22236
  detachEvent(badgeElem, 'click', click);
22237
+ detachEvent(badgeElem, 'mousedown', preserveFocus);
22172
22238
  attached = false;
22173
22239
  });
22174
22240
  return badge;
@@ -22639,7 +22705,7 @@ function stepShowingProc(guide, step) {
22639
22705
  return;
22640
22706
  }
22641
22707
  const embedSelector = _.get(guide, 'attributes.embedConfig.selector');
22642
- if (embedSelector && !dom(embedSelector).length) {
22708
+ if (embedSelector && (!dom(embedSelector).length || (guideElement && !isInDocument(guideElement[0])))) {
22643
22709
  step.hide();
22644
22710
  return;
22645
22711
  }
@@ -25383,6 +25449,9 @@ var initGuides = function (observer) {
25383
25449
  return;
25384
25450
  guides ? enableGuides() : disableGuides();
25385
25451
  });
25452
+ if (isGuideConsentDenied()) {
25453
+ disableGuides();
25454
+ }
25386
25455
  teardownFns.push(onTurbolinksPageLoad(document, function onPageLoad() {
25387
25456
  if (pendoDotUrl.get() === reloadGuides.lastUrl) {
25388
25457
  // Force a reload if Turbolinks replaces the document body without changing the url
@@ -26044,10 +26113,14 @@ function isPortValid(port) {
26044
26113
  // (APP-168708). frames and its indexed children are readable across origins, so
26045
26114
  // this holds where an origin check can't: no ancestorOrigins needed, and sandboxed
26046
26115
  // frames (origin "null") are still found.
26116
+ //
26117
+ // frames skips iframes whose element lives in a shadow root, so a web-component
26118
+ // modal's frame never turns up in the walk. top is also readable across origins
26119
+ // and can't be spoofed by a popup, so it covers those frames (APP-171528).
26047
26120
  function isFrameBelowUs(source, window) {
26048
26121
  if (!source)
26049
26122
  return false;
26050
- if (source === window)
26123
+ if (source === window || source.top === window)
26051
26124
  return true;
26052
26125
  var pending = [window];
26053
26126
  while (pending.length) {
@@ -29325,6 +29398,7 @@ const PluginAPI = {
29325
29398
  q,
29326
29399
  SendQueue,
29327
29400
  sessionStorage: agentStorage.getSession(),
29401
+ storageIsDisabled,
29328
29402
  sizeElements,
29329
29403
  store,
29330
29404
  Tombstone,
@@ -29503,6 +29577,34 @@ function initIdentityEvents() {
29503
29577
  };
29504
29578
  }
29505
29579
 
29580
+ /**
29581
+ * Enforces the cookie consent policy a visitor has agreed to. The policy is defined by the subscription's cookie consent configuration,
29582
+ * and is used to determine whether each product the web SDK may run.
29583
+ *
29584
+ * @access public
29585
+ * @category Core
29586
+ * @param {string | string[]} tiers tier or tiers the visitor consented to
29587
+ * @returns {object} map of product to its resolved value -- `guides` and `identity` are
29588
+ * booleans (identity disabled means DNT), `textCollection` is `none`/`whitelist`/`all`,
29589
+ * `analytics` and `guideAnalytics` are each an array of allowed event types
29590
+ * @example
29591
+ * pendo.updateCookieConsent('strictlyNecessary');
29592
+ * @example
29593
+ * pendo.updateCookieConsent(['strictlyNecessary', 'functional']);
29594
+ */
29595
+ function updateCookieConsent(tiers) {
29596
+ const enabledProducts = applyConsentTiers(tiers);
29597
+ Events['consent:changed'].trigger(enabledProducts);
29598
+ log.debug('Cookie consent updated for ' + JSON.stringify(tiers) + '.');
29599
+ return enabledProducts;
29600
+ }
29601
+ function initializeCookieConsent(tiers) {
29602
+ resetConsentPolicy();
29603
+ if (tiers) {
29604
+ updateCookieConsent(tiers);
29605
+ }
29606
+ }
29607
+
29506
29608
  /*
29507
29609
  * ResourceCenterActivity Event
29508
29610
  *
@@ -29986,6 +30088,8 @@ function initialize(options) {
29986
30088
  }
29987
30089
  // Save the options somewhere
29988
30090
  ConfigReader.setLocalConfig(options);
30091
+ const cookieConsentTiers = _.get(options, 'cookieConsentTiers');
30092
+ initializeCookieConsent(cookieConsentTiers);
29989
30093
  initDataHost();
29990
30094
  exportDataHost(pendo$1);
29991
30095
  const cookieDomain = ConfigReader.get('cookieDomain');
@@ -31232,28 +31336,6 @@ logPublic.setActiveContexts = setActiveContexts;
31232
31336
  logPublic.showLogHistory = showLogHistory;
31233
31337
  logPublic.getLoggedContexts = getLoggedContexts;
31234
31338
 
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
31339
  /**
31258
31340
  * Shared helper for event collection
31259
31341
  */
@@ -42735,6 +42817,310 @@ var guideMarkdownUtil = {
42735
42817
  removeMarkdownSyntax
42736
42818
  };
42737
42819
 
42820
+ const AUTO_ADVANCE_DELAY_MS = 250;
42821
+ function isAutoAdvanceRequested(step, _) {
42822
+ return _.get(step, 'attributes.autoAdvance') === true;
42823
+ }
42824
+ // Survey-authored DOM carries no question-type classes, so eligibility keys off
42825
+ // answer-control shape: every required poll id must be radio-backed, which also
42826
+ // refuses a future non-radio type by default.
42827
+ function isStepEligibleInDom(step, _, requiredPollIds) {
42828
+ const guideElement = step && step.guideElement;
42829
+ if (!guideElement)
42830
+ return false;
42831
+ if (guideElement.find('textarea, input:text, input:checkbox, select').length)
42832
+ return false;
42833
+ const pollIds = _.uniq(_.map(guideElement.find('[data-pendo-poll-id]'), (element) => element.getAttribute('data-pendo-poll-id')));
42834
+ if (!pollIds.length)
42835
+ return false;
42836
+ return _.all(pollIds, (pollId) => _.contains(requiredPollIds, pollId) &&
42837
+ guideElement.find(`input:radio[data-pendo-poll-id="${pollId}"]`).length > 0);
42838
+ }
42839
+ function hideSubmitButtons(step, _, submitButtons) {
42840
+ const hiddenButtons = [];
42841
+ _.each(submitButtons, (button) => {
42842
+ const element = step.guideElement.find(`#${button.props.id}`)[0];
42843
+ if (!element)
42844
+ return;
42845
+ // display, not visibility: a reserved-but-invisible box leaves an empty gap
42846
+ // at the foot of the step. Authored styles reach the button inline
42847
+ // (build.js:151), so the original value is kept rather than blanked.
42848
+ hiddenButtons.push({ element, display: element.style.display });
42849
+ element.style.display = 'none';
42850
+ });
42851
+ return function restoreSubmitButtons() {
42852
+ _.each(hiddenButtons, (hiddenButton) => {
42853
+ hiddenButton.element.style.display = hiddenButton.display;
42854
+ });
42855
+ };
42856
+ }
42857
+ // The container's pixel height is measured during render (layout.js) before custom
42858
+ // scripts run, so collapsing a row afterwards leaves the box stale and the step keeps
42859
+ // the empty space. Same reflow pair metadata-substitution uses, scoped to this step so
42860
+ // a second guide on screen is left alone.
42861
+ function reflowGuide(step, _, pendo) {
42862
+ const recalculateGuideHeight = _.get(pendo, 'BuildingBlocks.BuildingBlockGuides.recalculateGuideHeight');
42863
+ try {
42864
+ if (_.isFunction(pendo.flexElement)) {
42865
+ pendo.flexElement(step.guideElement.find(guideMarkdownUtil.containerSelector));
42866
+ }
42867
+ if (_.isFunction(recalculateGuideHeight)) {
42868
+ recalculateGuideHeight(`pendo-g-${step.id}`, step.guideElement);
42869
+ }
42870
+ }
42871
+ catch (error) {
42872
+ pendo.log('[auto-advance] guide reflow failed, the step may keep an empty row', error && error.message);
42873
+ }
42874
+ }
42875
+ function createInertController() {
42876
+ return { isActive: false, notifyAnswered() { }, cancel() { } };
42877
+ }
42878
+ function createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds) {
42879
+ const _ = pendo._;
42880
+ const inert = createInertController();
42881
+ if (!isAutoAdvanceRequested(step, _))
42882
+ return inert;
42883
+ // Only reachable for API-authored guides; choosing between two
42884
+ // submitPollAndGoToStep targets would be a guess.
42885
+ if (_.size(submitButtons) !== 1) {
42886
+ pendo.log(`[auto-advance] not arming: step ${step.id} needs exactly one submit button, found ${_.size(submitButtons)}`);
42887
+ return inert;
42888
+ }
42889
+ if (!isStepEligibleInDom(step, _, requiredPollIds)) {
42890
+ pendo.log(`[auto-advance] not arming: rendered step ${step.id} failed the eligibility re-check`);
42891
+ return inert;
42892
+ }
42893
+ const submitButton = submitButtons[0];
42894
+ // A click binds every action on the node and dispatches one event each
42895
+ // (build.js:205-207); we dispatch one, so a second action would be dropped.
42896
+ if (_.size(submitButton.actions) !== 1) {
42897
+ pendo.log(`[auto-advance] not arming: step ${step.id} submit button carries ${_.size(submitButton.actions)} actions, expected one`);
42898
+ return inert;
42899
+ }
42900
+ const submitAction = _.find(submitButton.actions, (action) => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep');
42901
+ if (!submitAction)
42902
+ return inert;
42903
+ let hasFired = false;
42904
+ let restoreSubmitButtons = _.noop;
42905
+ function fire() {
42906
+ if (hasFired)
42907
+ return;
42908
+ hasFired = true;
42909
+ try {
42910
+ // Shape-identical to what bindActionToNode emits for a click, so
42911
+ // beforeAdvance consumers intercept an auto-advance the same way.
42912
+ step.eventRouter.eventable.trigger('pendoEvent', {
42913
+ action: submitAction.action,
42914
+ params: submitAction.parameters,
42915
+ step,
42916
+ ignore: !!submitAction.ignore,
42917
+ srcElement: step.guideElement.find(`#${submitButton.props.id}`)[0]
42918
+ });
42919
+ }
42920
+ catch (error) {
42921
+ restoreSubmitButtons();
42922
+ reflowGuide(step, _, pendo);
42923
+ pendo.log('[auto-advance] submit dispatch failed, restored the submit button', error && error.message);
42924
+ }
42925
+ }
42926
+ const debouncedFire = _.debounce(fire, AUTO_ADVANCE_DELAY_MS);
42927
+ // Last, so nothing between here and the return can throw and leave the step
42928
+ // with a hidden button and no controller to un-hide it.
42929
+ restoreSubmitButtons = hideSubmitButtons(step, _, submitButtons);
42930
+ reflowGuide(step, _, pendo);
42931
+ return {
42932
+ isActive: true,
42933
+ notifyAnswered: debouncedFire,
42934
+ cancel: debouncedFire.cancel
42935
+ };
42936
+ }
42937
+
42938
+ const SURVEY_ANSWERS_KEY = 'pendo_survey_answers';
42939
+ const ANSWER_CONTROLS = 'input:radio, input:text, textarea, select';
42940
+ const TEXT_CONTROLS = 'input:text, textarea';
42941
+ // Matches MULTISTEP_CONTINUATION_TIME_LIMIT (walkthrough-guide.js:67), past which a guide is not continued.
42942
+ const ANSWER_TTL_MS = 12 * 60 * 60 * 1000;
42943
+ let pluginApi = null;
42944
+ let inMemoryAnswers = {};
42945
+ let persistedAnswersDropped = false;
42946
+ // A restricted storage.allowKeys silently rejects the session write, so mirror the visit here.
42947
+ const visitedSteps = {};
42948
+ function setPluginApi(api) {
42949
+ pluginApi = api;
42950
+ if (!api || !api.agentStorage)
42951
+ return;
42952
+ /**
42953
+ * Answers the visitor has given on each step of a survey, so a step re-shown
42954
+ * after Previous comes back filled in rather than blank.
42955
+ *
42956
+ * @name pendo_survey_answers
42957
+ * @category Cookies/sessionStorage
42958
+ * @access public
42959
+ * @label SURVEY_ANSWERS_KEY
42960
+ */
42961
+ api.agentStorage.registry.addSession(SURVEY_ANSWERS_KEY);
42962
+ }
42963
+ // getSession() checks the allow-list but not storageIsDisabled, which the cookie path applies.
42964
+ function persistenceAllowed() {
42965
+ const allowed = !!pluginApi &&
42966
+ !!pluginApi.sessionStorage &&
42967
+ typeof pluginApi.storageIsDisabled === 'function' &&
42968
+ !pluginApi.storageIsDisabled();
42969
+ if (allowed) {
42970
+ persistedAnswersDropped = false;
42971
+ }
42972
+ else {
42973
+ dropPersistedAnswers();
42974
+ }
42975
+ return allowed;
42976
+ }
42977
+ // agentStorage.clearAll only walks local keys, so a session key has to be dropped here.
42978
+ function dropPersistedAnswers() {
42979
+ if (persistedAnswersDropped || !pluginApi || !pluginApi.sessionStorage)
42980
+ return;
42981
+ persistedAnswersDropped = true;
42982
+ pluginApi.sessionStorage.removeItem(SURVEY_ANSWERS_KEY);
42983
+ }
42984
+ function readAll() {
42985
+ if (!persistenceAllowed())
42986
+ return inMemoryAnswers;
42987
+ try {
42988
+ return JSON.parse(pluginApi.sessionStorage.getItem(SURVEY_ANSWERS_KEY)) || {};
42989
+ }
42990
+ catch (error) {
42991
+ return {};
42992
+ }
42993
+ }
42994
+ function writeAll(answers) {
42995
+ if (!persistenceAllowed()) {
42996
+ inMemoryAnswers = answers;
42997
+ return;
42998
+ }
42999
+ pluginApi.sessionStorage.setItem(SURVEY_ANSWERS_KEY, JSON.stringify(answers));
43000
+ }
43001
+ // identify() sets the new id before the reload tears the step down, so resolve this once per render.
43002
+ function visitorKey(pendo) {
43003
+ return (pendo._.isFunction(pendo.get_visitor_id) && pendo.get_visitor_id()) || 'unknown';
43004
+ }
43005
+ function stateKey(step, guide, visitor) {
43006
+ return `${visitor}:${guide.id}:${step.id}`;
43007
+ }
43008
+ // Host pages reassign Date.now to return a Date, which JSON writes out as a string.
43009
+ function getNow() {
43010
+ return new Date().getTime();
43011
+ }
43012
+ function isFresh(writtenAt) {
43013
+ return !!writtenAt && (getNow() - writtenAt) <= ANSWER_TTL_MS;
43014
+ }
43015
+ function readRecord(step, guide, visitor) {
43016
+ const forVisitor = readAll()[visitor];
43017
+ const forGuide = forVisitor && forVisitor[guide.id];
43018
+ const record = (forGuide && forGuide[step.id]) || null;
43019
+ return isFresh(record && record.at) ? record : null;
43020
+ }
43021
+ function updateRecord(step, guide, visitor, changes) {
43022
+ const all = readAll();
43023
+ const current = readRecord(step, guide, visitor) || {};
43024
+ if (!all[visitor])
43025
+ all[visitor] = {};
43026
+ if (!all[visitor][guide.id])
43027
+ all[visitor][guide.id] = {};
43028
+ all[visitor][guide.id][step.id] = {
43029
+ at: getNow(),
43030
+ visited: changes.visited || current.visited || 0,
43031
+ answers: changes.answers || current.answers || {}
43032
+ };
43033
+ writeAll(all);
43034
+ }
43035
+ // Previous only renders after an auto-advance step, so any other guide needs none of this.
43036
+ function guideAutoAdvances(guide, _) {
43037
+ return _.any(_.get(guide, 'steps', []), (step) => isAutoAdvanceRequested(step, _));
43038
+ }
43039
+ function pollIdFor(control) {
43040
+ let node = control;
43041
+ while (node && node.getAttribute) {
43042
+ const pollId = node.getAttribute('data-pendo-poll-id');
43043
+ if (pollId)
43044
+ return pollId;
43045
+ node = node.parentNode;
43046
+ }
43047
+ return null;
43048
+ }
43049
+ // submitPoll never reads a checkbox back (event-router.js:264), so restoring one shows nothing sent.
43050
+ function isRadio(control) {
43051
+ return control.type === 'radio';
43052
+ }
43053
+ function collectAnswers(step, pendo) {
43054
+ const answers = {};
43055
+ pendo._.each(step.guideElement.find(ANSWER_CONTROLS), (control) => {
43056
+ const pollId = pollIdFor(control);
43057
+ // One value per poll id, first in document order, as submitPoll reads it.
43058
+ if (!pollId || answers[pollId])
43059
+ return;
43060
+ if (isRadio(control) && !control.checked)
43061
+ return;
43062
+ if (!control.value)
43063
+ return;
43064
+ answers[pollId] = [control.value];
43065
+ });
43066
+ return answers;
43067
+ }
43068
+ // No event dispatched: a restored answer must not reach the gate listeners or re-arm auto-advance.
43069
+ function restoreAnswers(step, pendo, answers) {
43070
+ pendo._.each(step.guideElement.find(ANSWER_CONTROLS), (control) => {
43071
+ const pollId = pollIdFor(control);
43072
+ const values = pollId && answers[pollId];
43073
+ if (!values)
43074
+ return;
43075
+ if (isRadio(control)) {
43076
+ if (pendo._.contains(values, control.value))
43077
+ control.checked = true;
43078
+ return;
43079
+ }
43080
+ control.value = values[0];
43081
+ });
43082
+ }
43083
+ function isRevisit(step, guide, pendo) {
43084
+ if (!guideAutoAdvances(guide, pendo._))
43085
+ return false;
43086
+ const visitor = visitorKey(pendo);
43087
+ if (isFresh(visitedSteps[stateKey(step, guide, visitor)]))
43088
+ return true;
43089
+ const record = readRecord(step, guide, visitor);
43090
+ return !!record && (!!record.visited || !pendo._.isEmpty(record.answers));
43091
+ }
43092
+ const AnswerCache = {
43093
+ name: 'AnswerCache',
43094
+ script(step, guide, pendo) {
43095
+ const visitor = visitorKey(pendo);
43096
+ const record = readRecord(step, guide, visitor);
43097
+ restoreAnswers(step, pendo, (record && record.answers) || {});
43098
+ // Marked on leaving, not on render: sessionStorage survives a reload, which is not a revisit.
43099
+ this.on('beforeUnmount', (event) => {
43100
+ if (event && event.reason === 'hidden')
43101
+ return;
43102
+ visitedSteps[stateKey(step, guide, visitor)] = getNow();
43103
+ updateRecord(step, guide, visitor, { visited: 1 });
43104
+ });
43105
+ const stepRoot = step.guideElement[0];
43106
+ // change does not bubble on legacy browsers; it is here only for keyboard selection.
43107
+ step.attachEvent(stepRoot, 'click', capture);
43108
+ step.attachEvent(stepRoot, 'change', capture);
43109
+ pendo._.each(step.guideElement.find(TEXT_CONTROLS), (control) => {
43110
+ step.attachEvent(control, 'input', capture);
43111
+ });
43112
+ function capture() {
43113
+ updateRecord(step, guide, visitor, { answers: collectAnswers(step, pendo) });
43114
+ }
43115
+ },
43116
+ test(step, guide, pendo) {
43117
+ const guideElement = step && step.guideElement;
43118
+ return !!guideElement &&
43119
+ guideAutoAdvances(guide, pendo._) &&
43120
+ guideElement.find(ANSWER_CONTROLS).length > 0;
43121
+ }
43122
+ };
43123
+
42738
43124
  function getZoneSafeMethod(_, method, target = window) {
42739
43125
  var zoneSymbol = '__symbol__';
42740
43126
  /* global Zone */
@@ -43211,124 +43597,6 @@ function substitutionIcon(color, size) {
43211
43597
  13.7239 15.5 14V17Z" fill="${color}"></path> </g></svg></div>`);
43212
43598
  }
43213
43599
 
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
43600
  const requiredElement = '<span class="_pendo-required-indicator" style="color:red; font-style:italic;" title="Question is required"> *</span>';
43333
43601
  const requiredSyntax = '{required/}';
43334
43602
  const RequiredQuestions = {
@@ -43354,9 +43622,14 @@ const RequiredQuestions = {
43354
43622
  }
43355
43623
  });
43356
43624
  }
43625
+ // Restored answers reach no listener, and the controller is still inert here.
43626
+ const revisited = isRevisit(step, guide, pendo);
43627
+ if (revisited) {
43628
+ evaluateRequiredQuestions(requiredQuestions);
43629
+ }
43357
43630
  // Armed last, after the listeners above: runCustomScripts abandons the rest of
43358
43631
  // this script on a throw, and the submit button is already disabled by now.
43359
- if (isAutoAdvanceRequested(step, pendo._)) {
43632
+ if (isAutoAdvanceRequested(step, pendo._) && !revisited) {
43360
43633
  this.on('beforeUnmount', () => autoAdvance.cancel());
43361
43634
  try {
43362
43635
  autoAdvance = createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds);
@@ -43707,7 +43980,9 @@ function GuideMarkdown() {
43707
43980
  const markdownScriptsEnabled = configReader.get(GUIDEMARKDOWN_CONFIG);
43708
43981
  if (!markdownScriptsEnabled)
43709
43982
  return;
43710
- guideMarkdown = [PollBranching, MetadataSubstitution, RequiredQuestions, SkipToEligibleStep];
43983
+ setPluginApi(PluginAPI);
43984
+ // AnswerCache first: it restores the answers RequiredQuestions reads the gate against.
43985
+ guideMarkdown = [AnswerCache, PollBranching, MetadataSubstitution, RequiredQuestions, SkipToEligibleStep];
43711
43986
  if (!pendo.designer) {
43712
43987
  pendo._.each(guideMarkdown, function (script) {
43713
43988
  PluginAPI.GlobalRuntime.addGlobalScript(script);
@@ -51498,11 +51773,13 @@ class Transport {
51498
51773
  }
51499
51774
  }
51500
51775
 
51501
- const CATCHALL_TYPES = ['load', 'click', 'track'];
51776
+ const CATCHALL_TYPES = ['load', 'click', 'track', 'guideSeen'];
51777
+ const AGENTIC_TYPES = ["prompt", "agent_response", "user_reaction"];
51502
51778
  const EVENT_KINDS = {
51503
- catchall: 'catchall',
51504
- devlog: 'devlog',
51505
- recording: 'recording'
51779
+ AGENTIC: 'agentic',
51780
+ CATCHALL: 'catchall',
51781
+ DEVLOG: 'devlog',
51782
+ RECORDING: 'recording'
51506
51783
  };
51507
51784
  class LiveReplayClient {
51508
51785
  constructor(pendo, api) {
@@ -51583,18 +51860,21 @@ class LiveReplayClient {
51583
51860
  catch (_a) { }
51584
51861
  }
51585
51862
  sendRecordingEvent(fields) {
51586
- this.send(Object.assign(Object.assign({}, fields), { kind: EVENT_KINDS.recording, sequence: this.sequence++ }));
51863
+ this.send(Object.assign(Object.assign({}, fields), { kind: EVENT_KINDS.RECORDING, sequence: this.sequence++ }));
51587
51864
  }
51588
51865
  forwardEvent(event) {
51589
51866
  if (!event || !event.data || !event.data.length)
51590
51867
  return;
51591
51868
  const capturedEvent = event.data[0];
51592
51869
  const config = { event: capturedEvent };
51870
+ if (this.pendo._.contains(AGENTIC_TYPES, capturedEvent.type)) {
51871
+ config.kind = EVENT_KINDS.AGENTIC;
51872
+ }
51593
51873
  if (this.pendo._.contains(CATCHALL_TYPES, capturedEvent.type)) {
51594
- config.kind = EVENT_KINDS.catchall;
51874
+ config.kind = EVENT_KINDS.CATCHALL;
51595
51875
  }
51596
- if (capturedEvent.type === EVENT_KINDS.devlog) {
51597
- config.kind = EVENT_KINDS.devlog;
51876
+ if (capturedEvent.type === EVENT_KINDS.DEVLOG) {
51877
+ config.kind = EVENT_KINDS.DEVLOG;
51598
51878
  }
51599
51879
  if (!config.kind)
51600
51880
  return;
@@ -52181,7 +52461,6 @@ class SessionRecorder {
52181
52461
  var bind = this.pendo._.bind;
52182
52462
  this.visitorConfig = visitorConfig;
52183
52463
  this.clearOldSessionId();
52184
- this.liveReplayClient.onLiveFlag(visitorConfig.live, this.visitorId);
52185
52464
  this.sendQueue.start();
52186
52465
  this.interval = setInterval(() => {
52187
52466
  if (!this.sendQueue.failed()) {
@@ -52194,6 +52473,7 @@ class SessionRecorder {
52194
52473
  errorHandler: this.errorHandler
52195
52474
  }, config));
52196
52475
  this._refreshIds();
52476
+ this.liveReplayClient.onLiveFlag(visitorConfig.live, this.visitorId, () => this.snapshot());
52197
52477
  this.onRecordingStart();
52198
52478
  }
52199
52479
  catch (e) {