@pendo/agent 2.342.0 → 2.343.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.
@@ -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.0_';
4062
+ let PACKAGE_VERSION = '2.343.0';
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
@@ -29325,6 +29394,7 @@ const PluginAPI = {
29325
29394
  q,
29326
29395
  SendQueue,
29327
29396
  sessionStorage: agentStorage.getSession(),
29397
+ storageIsDisabled,
29328
29398
  sizeElements,
29329
29399
  store,
29330
29400
  Tombstone,
@@ -29503,6 +29573,34 @@ function initIdentityEvents() {
29503
29573
  };
29504
29574
  }
29505
29575
 
29576
+ /**
29577
+ * Enforces the cookie consent policy a visitor has agreed to. The policy is defined by the subscription's cookie consent configuration,
29578
+ * and is used to determine whether each product the web SDK may run.
29579
+ *
29580
+ * @access public
29581
+ * @category Core
29582
+ * @param {string | string[]} tiers tier or tiers the visitor consented to
29583
+ * @returns {object} map of product to its resolved value -- `guides` and `identity` are
29584
+ * booleans (identity disabled means DNT), `textCollection` is `none`/`whitelist`/`all`,
29585
+ * `analytics` and `guideAnalytics` are each an array of allowed event types
29586
+ * @example
29587
+ * pendo.updateCookieConsent('strictlyNecessary');
29588
+ * @example
29589
+ * pendo.updateCookieConsent(['strictlyNecessary', 'functional']);
29590
+ */
29591
+ function updateCookieConsent(tiers) {
29592
+ const enabledProducts = applyConsentTiers(tiers);
29593
+ Events['consent:changed'].trigger(enabledProducts);
29594
+ log.debug('Cookie consent updated for ' + JSON.stringify(tiers) + '.');
29595
+ return enabledProducts;
29596
+ }
29597
+ function initializeCookieConsent(tiers) {
29598
+ resetConsentPolicy();
29599
+ if (tiers) {
29600
+ updateCookieConsent(tiers);
29601
+ }
29602
+ }
29603
+
29506
29604
  /*
29507
29605
  * ResourceCenterActivity Event
29508
29606
  *
@@ -29986,6 +30084,8 @@ function initialize(options) {
29986
30084
  }
29987
30085
  // Save the options somewhere
29988
30086
  ConfigReader.setLocalConfig(options);
30087
+ const cookieConsentTiers = _.get(options, 'cookieConsentTiers');
30088
+ initializeCookieConsent(cookieConsentTiers);
29989
30089
  initDataHost();
29990
30090
  exportDataHost(pendo$1);
29991
30091
  const cookieDomain = ConfigReader.get('cookieDomain');
@@ -31232,28 +31332,6 @@ logPublic.setActiveContexts = setActiveContexts;
31232
31332
  logPublic.showLogHistory = showLogHistory;
31233
31333
  logPublic.getLoggedContexts = getLoggedContexts;
31234
31334
 
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
31335
  /**
31258
31336
  * Shared helper for event collection
31259
31337
  */
@@ -42735,6 +42813,310 @@ var guideMarkdownUtil = {
42735
42813
  removeMarkdownSyntax
42736
42814
  };
42737
42815
 
42816
+ const AUTO_ADVANCE_DELAY_MS = 250;
42817
+ function isAutoAdvanceRequested(step, _) {
42818
+ return _.get(step, 'attributes.autoAdvance') === true;
42819
+ }
42820
+ // Survey-authored DOM carries no question-type classes, so eligibility keys off
42821
+ // answer-control shape: every required poll id must be radio-backed, which also
42822
+ // refuses a future non-radio type by default.
42823
+ function isStepEligibleInDom(step, _, requiredPollIds) {
42824
+ const guideElement = step && step.guideElement;
42825
+ if (!guideElement)
42826
+ return false;
42827
+ if (guideElement.find('textarea, input:text, input:checkbox, select').length)
42828
+ return false;
42829
+ const pollIds = _.uniq(_.map(guideElement.find('[data-pendo-poll-id]'), (element) => element.getAttribute('data-pendo-poll-id')));
42830
+ if (!pollIds.length)
42831
+ return false;
42832
+ return _.all(pollIds, (pollId) => _.contains(requiredPollIds, pollId) &&
42833
+ guideElement.find(`input:radio[data-pendo-poll-id="${pollId}"]`).length > 0);
42834
+ }
42835
+ function hideSubmitButtons(step, _, submitButtons) {
42836
+ const hiddenButtons = [];
42837
+ _.each(submitButtons, (button) => {
42838
+ const element = step.guideElement.find(`#${button.props.id}`)[0];
42839
+ if (!element)
42840
+ return;
42841
+ // display, not visibility: a reserved-but-invisible box leaves an empty gap
42842
+ // at the foot of the step. Authored styles reach the button inline
42843
+ // (build.js:151), so the original value is kept rather than blanked.
42844
+ hiddenButtons.push({ element, display: element.style.display });
42845
+ element.style.display = 'none';
42846
+ });
42847
+ return function restoreSubmitButtons() {
42848
+ _.each(hiddenButtons, (hiddenButton) => {
42849
+ hiddenButton.element.style.display = hiddenButton.display;
42850
+ });
42851
+ };
42852
+ }
42853
+ // The container's pixel height is measured during render (layout.js) before custom
42854
+ // scripts run, so collapsing a row afterwards leaves the box stale and the step keeps
42855
+ // the empty space. Same reflow pair metadata-substitution uses, scoped to this step so
42856
+ // a second guide on screen is left alone.
42857
+ function reflowGuide(step, _, pendo) {
42858
+ const recalculateGuideHeight = _.get(pendo, 'BuildingBlocks.BuildingBlockGuides.recalculateGuideHeight');
42859
+ try {
42860
+ if (_.isFunction(pendo.flexElement)) {
42861
+ pendo.flexElement(step.guideElement.find(guideMarkdownUtil.containerSelector));
42862
+ }
42863
+ if (_.isFunction(recalculateGuideHeight)) {
42864
+ recalculateGuideHeight(`pendo-g-${step.id}`, step.guideElement);
42865
+ }
42866
+ }
42867
+ catch (error) {
42868
+ pendo.log('[auto-advance] guide reflow failed, the step may keep an empty row', error && error.message);
42869
+ }
42870
+ }
42871
+ function createInertController() {
42872
+ return { isActive: false, notifyAnswered() { }, cancel() { } };
42873
+ }
42874
+ function createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds) {
42875
+ const _ = pendo._;
42876
+ const inert = createInertController();
42877
+ if (!isAutoAdvanceRequested(step, _))
42878
+ return inert;
42879
+ // Only reachable for API-authored guides; choosing between two
42880
+ // submitPollAndGoToStep targets would be a guess.
42881
+ if (_.size(submitButtons) !== 1) {
42882
+ pendo.log(`[auto-advance] not arming: step ${step.id} needs exactly one submit button, found ${_.size(submitButtons)}`);
42883
+ return inert;
42884
+ }
42885
+ if (!isStepEligibleInDom(step, _, requiredPollIds)) {
42886
+ pendo.log(`[auto-advance] not arming: rendered step ${step.id} failed the eligibility re-check`);
42887
+ return inert;
42888
+ }
42889
+ const submitButton = submitButtons[0];
42890
+ // A click binds every action on the node and dispatches one event each
42891
+ // (build.js:205-207); we dispatch one, so a second action would be dropped.
42892
+ if (_.size(submitButton.actions) !== 1) {
42893
+ pendo.log(`[auto-advance] not arming: step ${step.id} submit button carries ${_.size(submitButton.actions)} actions, expected one`);
42894
+ return inert;
42895
+ }
42896
+ const submitAction = _.find(submitButton.actions, (action) => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep');
42897
+ if (!submitAction)
42898
+ return inert;
42899
+ let hasFired = false;
42900
+ let restoreSubmitButtons = _.noop;
42901
+ function fire() {
42902
+ if (hasFired)
42903
+ return;
42904
+ hasFired = true;
42905
+ try {
42906
+ // Shape-identical to what bindActionToNode emits for a click, so
42907
+ // beforeAdvance consumers intercept an auto-advance the same way.
42908
+ step.eventRouter.eventable.trigger('pendoEvent', {
42909
+ action: submitAction.action,
42910
+ params: submitAction.parameters,
42911
+ step,
42912
+ ignore: !!submitAction.ignore,
42913
+ srcElement: step.guideElement.find(`#${submitButton.props.id}`)[0]
42914
+ });
42915
+ }
42916
+ catch (error) {
42917
+ restoreSubmitButtons();
42918
+ reflowGuide(step, _, pendo);
42919
+ pendo.log('[auto-advance] submit dispatch failed, restored the submit button', error && error.message);
42920
+ }
42921
+ }
42922
+ const debouncedFire = _.debounce(fire, AUTO_ADVANCE_DELAY_MS);
42923
+ // Last, so nothing between here and the return can throw and leave the step
42924
+ // with a hidden button and no controller to un-hide it.
42925
+ restoreSubmitButtons = hideSubmitButtons(step, _, submitButtons);
42926
+ reflowGuide(step, _, pendo);
42927
+ return {
42928
+ isActive: true,
42929
+ notifyAnswered: debouncedFire,
42930
+ cancel: debouncedFire.cancel
42931
+ };
42932
+ }
42933
+
42934
+ const SURVEY_ANSWERS_KEY = 'pendo_survey_answers';
42935
+ const ANSWER_CONTROLS = 'input:radio, input:text, textarea, select';
42936
+ const TEXT_CONTROLS = 'input:text, textarea';
42937
+ // Matches MULTISTEP_CONTINUATION_TIME_LIMIT (walkthrough-guide.js:67), past which a guide is not continued.
42938
+ const ANSWER_TTL_MS = 12 * 60 * 60 * 1000;
42939
+ let pluginApi = null;
42940
+ let inMemoryAnswers = {};
42941
+ let persistedAnswersDropped = false;
42942
+ // A restricted storage.allowKeys silently rejects the session write, so mirror the visit here.
42943
+ const visitedSteps = {};
42944
+ function setPluginApi(api) {
42945
+ pluginApi = api;
42946
+ if (!api || !api.agentStorage)
42947
+ return;
42948
+ /**
42949
+ * Answers the visitor has given on each step of a survey, so a step re-shown
42950
+ * after Previous comes back filled in rather than blank.
42951
+ *
42952
+ * @name pendo_survey_answers
42953
+ * @category Cookies/sessionStorage
42954
+ * @access public
42955
+ * @label SURVEY_ANSWERS_KEY
42956
+ */
42957
+ api.agentStorage.registry.addSession(SURVEY_ANSWERS_KEY);
42958
+ }
42959
+ // getSession() checks the allow-list but not storageIsDisabled, which the cookie path applies.
42960
+ function persistenceAllowed() {
42961
+ const allowed = !!pluginApi &&
42962
+ !!pluginApi.sessionStorage &&
42963
+ typeof pluginApi.storageIsDisabled === 'function' &&
42964
+ !pluginApi.storageIsDisabled();
42965
+ if (allowed) {
42966
+ persistedAnswersDropped = false;
42967
+ }
42968
+ else {
42969
+ dropPersistedAnswers();
42970
+ }
42971
+ return allowed;
42972
+ }
42973
+ // agentStorage.clearAll only walks local keys, so a session key has to be dropped here.
42974
+ function dropPersistedAnswers() {
42975
+ if (persistedAnswersDropped || !pluginApi || !pluginApi.sessionStorage)
42976
+ return;
42977
+ persistedAnswersDropped = true;
42978
+ pluginApi.sessionStorage.removeItem(SURVEY_ANSWERS_KEY);
42979
+ }
42980
+ function readAll() {
42981
+ if (!persistenceAllowed())
42982
+ return inMemoryAnswers;
42983
+ try {
42984
+ return JSON.parse(pluginApi.sessionStorage.getItem(SURVEY_ANSWERS_KEY)) || {};
42985
+ }
42986
+ catch (error) {
42987
+ return {};
42988
+ }
42989
+ }
42990
+ function writeAll(answers) {
42991
+ if (!persistenceAllowed()) {
42992
+ inMemoryAnswers = answers;
42993
+ return;
42994
+ }
42995
+ pluginApi.sessionStorage.setItem(SURVEY_ANSWERS_KEY, JSON.stringify(answers));
42996
+ }
42997
+ // identify() sets the new id before the reload tears the step down, so resolve this once per render.
42998
+ function visitorKey(pendo) {
42999
+ return (pendo._.isFunction(pendo.get_visitor_id) && pendo.get_visitor_id()) || 'unknown';
43000
+ }
43001
+ function stateKey(step, guide, visitor) {
43002
+ return `${visitor}:${guide.id}:${step.id}`;
43003
+ }
43004
+ // Host pages reassign Date.now to return a Date, which JSON writes out as a string.
43005
+ function getNow() {
43006
+ return new Date().getTime();
43007
+ }
43008
+ function isFresh(writtenAt) {
43009
+ return !!writtenAt && (getNow() - writtenAt) <= ANSWER_TTL_MS;
43010
+ }
43011
+ function readRecord(step, guide, visitor) {
43012
+ const forVisitor = readAll()[visitor];
43013
+ const forGuide = forVisitor && forVisitor[guide.id];
43014
+ const record = (forGuide && forGuide[step.id]) || null;
43015
+ return isFresh(record && record.at) ? record : null;
43016
+ }
43017
+ function updateRecord(step, guide, visitor, changes) {
43018
+ const all = readAll();
43019
+ const current = readRecord(step, guide, visitor) || {};
43020
+ if (!all[visitor])
43021
+ all[visitor] = {};
43022
+ if (!all[visitor][guide.id])
43023
+ all[visitor][guide.id] = {};
43024
+ all[visitor][guide.id][step.id] = {
43025
+ at: getNow(),
43026
+ visited: changes.visited || current.visited || 0,
43027
+ answers: changes.answers || current.answers || {}
43028
+ };
43029
+ writeAll(all);
43030
+ }
43031
+ // Previous only renders after an auto-advance step, so any other guide needs none of this.
43032
+ function guideAutoAdvances(guide, _) {
43033
+ return _.any(_.get(guide, 'steps', []), (step) => isAutoAdvanceRequested(step, _));
43034
+ }
43035
+ function pollIdFor(control) {
43036
+ let node = control;
43037
+ while (node && node.getAttribute) {
43038
+ const pollId = node.getAttribute('data-pendo-poll-id');
43039
+ if (pollId)
43040
+ return pollId;
43041
+ node = node.parentNode;
43042
+ }
43043
+ return null;
43044
+ }
43045
+ // submitPoll never reads a checkbox back (event-router.js:264), so restoring one shows nothing sent.
43046
+ function isRadio(control) {
43047
+ return control.type === 'radio';
43048
+ }
43049
+ function collectAnswers(step, pendo) {
43050
+ const answers = {};
43051
+ pendo._.each(step.guideElement.find(ANSWER_CONTROLS), (control) => {
43052
+ const pollId = pollIdFor(control);
43053
+ // One value per poll id, first in document order, as submitPoll reads it.
43054
+ if (!pollId || answers[pollId])
43055
+ return;
43056
+ if (isRadio(control) && !control.checked)
43057
+ return;
43058
+ if (!control.value)
43059
+ return;
43060
+ answers[pollId] = [control.value];
43061
+ });
43062
+ return answers;
43063
+ }
43064
+ // No event dispatched: a restored answer must not reach the gate listeners or re-arm auto-advance.
43065
+ function restoreAnswers(step, pendo, answers) {
43066
+ pendo._.each(step.guideElement.find(ANSWER_CONTROLS), (control) => {
43067
+ const pollId = pollIdFor(control);
43068
+ const values = pollId && answers[pollId];
43069
+ if (!values)
43070
+ return;
43071
+ if (isRadio(control)) {
43072
+ if (pendo._.contains(values, control.value))
43073
+ control.checked = true;
43074
+ return;
43075
+ }
43076
+ control.value = values[0];
43077
+ });
43078
+ }
43079
+ function isRevisit(step, guide, pendo) {
43080
+ if (!guideAutoAdvances(guide, pendo._))
43081
+ return false;
43082
+ const visitor = visitorKey(pendo);
43083
+ if (isFresh(visitedSteps[stateKey(step, guide, visitor)]))
43084
+ return true;
43085
+ const record = readRecord(step, guide, visitor);
43086
+ return !!record && (!!record.visited || !pendo._.isEmpty(record.answers));
43087
+ }
43088
+ const AnswerCache = {
43089
+ name: 'AnswerCache',
43090
+ script(step, guide, pendo) {
43091
+ const visitor = visitorKey(pendo);
43092
+ const record = readRecord(step, guide, visitor);
43093
+ restoreAnswers(step, pendo, (record && record.answers) || {});
43094
+ // Marked on leaving, not on render: sessionStorage survives a reload, which is not a revisit.
43095
+ this.on('beforeUnmount', (event) => {
43096
+ if (event && event.reason === 'hidden')
43097
+ return;
43098
+ visitedSteps[stateKey(step, guide, visitor)] = getNow();
43099
+ updateRecord(step, guide, visitor, { visited: 1 });
43100
+ });
43101
+ const stepRoot = step.guideElement[0];
43102
+ // change does not bubble on legacy browsers; it is here only for keyboard selection.
43103
+ step.attachEvent(stepRoot, 'click', capture);
43104
+ step.attachEvent(stepRoot, 'change', capture);
43105
+ pendo._.each(step.guideElement.find(TEXT_CONTROLS), (control) => {
43106
+ step.attachEvent(control, 'input', capture);
43107
+ });
43108
+ function capture() {
43109
+ updateRecord(step, guide, visitor, { answers: collectAnswers(step, pendo) });
43110
+ }
43111
+ },
43112
+ test(step, guide, pendo) {
43113
+ const guideElement = step && step.guideElement;
43114
+ return !!guideElement &&
43115
+ guideAutoAdvances(guide, pendo._) &&
43116
+ guideElement.find(ANSWER_CONTROLS).length > 0;
43117
+ }
43118
+ };
43119
+
42738
43120
  function getZoneSafeMethod(_, method, target = window) {
42739
43121
  var zoneSymbol = '__symbol__';
42740
43122
  /* global Zone */
@@ -43211,124 +43593,6 @@ function substitutionIcon(color, size) {
43211
43593
  13.7239 15.5 14V17Z" fill="${color}"></path> </g></svg></div>`);
43212
43594
  }
43213
43595
 
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
43596
  const requiredElement = '<span class="_pendo-required-indicator" style="color:red; font-style:italic;" title="Question is required"> *</span>';
43333
43597
  const requiredSyntax = '{required/}';
43334
43598
  const RequiredQuestions = {
@@ -43354,9 +43618,14 @@ const RequiredQuestions = {
43354
43618
  }
43355
43619
  });
43356
43620
  }
43621
+ // Restored answers reach no listener, and the controller is still inert here.
43622
+ const revisited = isRevisit(step, guide, pendo);
43623
+ if (revisited) {
43624
+ evaluateRequiredQuestions(requiredQuestions);
43625
+ }
43357
43626
  // Armed last, after the listeners above: runCustomScripts abandons the rest of
43358
43627
  // this script on a throw, and the submit button is already disabled by now.
43359
- if (isAutoAdvanceRequested(step, pendo._)) {
43628
+ if (isAutoAdvanceRequested(step, pendo._) && !revisited) {
43360
43629
  this.on('beforeUnmount', () => autoAdvance.cancel());
43361
43630
  try {
43362
43631
  autoAdvance = createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds);
@@ -43707,7 +43976,9 @@ function GuideMarkdown() {
43707
43976
  const markdownScriptsEnabled = configReader.get(GUIDEMARKDOWN_CONFIG);
43708
43977
  if (!markdownScriptsEnabled)
43709
43978
  return;
43710
- guideMarkdown = [PollBranching, MetadataSubstitution, RequiredQuestions, SkipToEligibleStep];
43979
+ setPluginApi(PluginAPI);
43980
+ // AnswerCache first: it restores the answers RequiredQuestions reads the gate against.
43981
+ guideMarkdown = [AnswerCache, PollBranching, MetadataSubstitution, RequiredQuestions, SkipToEligibleStep];
43711
43982
  if (!pendo.designer) {
43712
43983
  pendo._.each(guideMarkdown, function (script) {
43713
43984
  PluginAPI.GlobalRuntime.addGlobalScript(script);
@@ -51498,11 +51769,13 @@ class Transport {
51498
51769
  }
51499
51770
  }
51500
51771
 
51501
- const CATCHALL_TYPES = ['load', 'click', 'track'];
51772
+ const CATCHALL_TYPES = ['load', 'click', 'track', 'guideSeen'];
51773
+ const AGENTIC_TYPES = ["prompt", "agent_response", "user_reaction"];
51502
51774
  const EVENT_KINDS = {
51503
- catchall: 'catchall',
51504
- devlog: 'devlog',
51505
- recording: 'recording'
51775
+ AGENTIC: 'agentic',
51776
+ CATCHALL: 'catchall',
51777
+ DEVLOG: 'devlog',
51778
+ RECORDING: 'recording'
51506
51779
  };
51507
51780
  class LiveReplayClient {
51508
51781
  constructor(pendo, api) {
@@ -51583,18 +51856,21 @@ class LiveReplayClient {
51583
51856
  catch (_a) { }
51584
51857
  }
51585
51858
  sendRecordingEvent(fields) {
51586
- this.send(Object.assign(Object.assign({}, fields), { kind: EVENT_KINDS.recording, sequence: this.sequence++ }));
51859
+ this.send(Object.assign(Object.assign({}, fields), { kind: EVENT_KINDS.RECORDING, sequence: this.sequence++ }));
51587
51860
  }
51588
51861
  forwardEvent(event) {
51589
51862
  if (!event || !event.data || !event.data.length)
51590
51863
  return;
51591
51864
  const capturedEvent = event.data[0];
51592
51865
  const config = { event: capturedEvent };
51866
+ if (this.pendo._.contains(AGENTIC_TYPES, capturedEvent.type)) {
51867
+ config.kind = EVENT_KINDS.AGENTIC;
51868
+ }
51593
51869
  if (this.pendo._.contains(CATCHALL_TYPES, capturedEvent.type)) {
51594
- config.kind = EVENT_KINDS.catchall;
51870
+ config.kind = EVENT_KINDS.CATCHALL;
51595
51871
  }
51596
- if (capturedEvent.type === EVENT_KINDS.devlog) {
51597
- config.kind = EVENT_KINDS.devlog;
51872
+ if (capturedEvent.type === EVENT_KINDS.DEVLOG) {
51873
+ config.kind = EVENT_KINDS.DEVLOG;
51598
51874
  }
51599
51875
  if (!config.kind)
51600
51876
  return;
@@ -52181,7 +52457,6 @@ class SessionRecorder {
52181
52457
  var bind = this.pendo._.bind;
52182
52458
  this.visitorConfig = visitorConfig;
52183
52459
  this.clearOldSessionId();
52184
- this.liveReplayClient.onLiveFlag(visitorConfig.live, this.visitorId);
52185
52460
  this.sendQueue.start();
52186
52461
  this.interval = setInterval(() => {
52187
52462
  if (!this.sendQueue.failed()) {
@@ -52194,6 +52469,7 @@ class SessionRecorder {
52194
52469
  errorHandler: this.errorHandler
52195
52470
  }, config));
52196
52471
  this._refreshIds();
52472
+ this.liveReplayClient.onLiveFlag(visitorConfig.live, this.visitorId, () => this.snapshot());
52197
52473
  this.onRecordingStart();
52198
52474
  }
52199
52475
  catch (e) {