@pendo/agent 2.328.2 → 2.330.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.
@@ -3403,7 +3403,7 @@ var ConfigReader = (function () {
3403
3403
  * cookies, localStorage, click tracking, and metadata. Only displays guides
3404
3404
  * that have been configured explicitly to show to this visitor.
3405
3405
  *
3406
- * @access private
3406
+ * @access public
3407
3407
  * @category Config/Guides
3408
3408
  * @name doNotTrackGuides
3409
3409
  * @default false
@@ -4010,8 +4010,8 @@ let SERVER = '';
4010
4010
  let ASSET_HOST = '';
4011
4011
  let ASSET_PATH = '';
4012
4012
  let DESIGNER_SERVER = '';
4013
- let VERSION = '2.328.2_';
4014
- let PACKAGE_VERSION = '2.328.2';
4013
+ let VERSION = '2.330.0_';
4014
+ let PACKAGE_VERSION = '2.330.0';
4015
4015
  let LOADER = 'xhr';
4016
4016
  /* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
4017
4017
  /**
@@ -9750,7 +9750,11 @@ function exportDataHost(pendo) {
9750
9750
  pendo.HOST = HOST;
9751
9751
  }
9752
9752
  function getAssetHost() {
9753
- return ConfigReader.get('assetHost') || getContentHost();
9753
+ var assetHost = ConfigReader.get('assetHost');
9754
+ if (assetHost) {
9755
+ return assetHost.indexOf('://') === -1 ? 'https://' + assetHost : assetHost;
9756
+ }
9757
+ return getContentHost();
9754
9758
  }
9755
9759
  function getContentHost() {
9756
9760
  var protocol = 'https://';
@@ -11752,13 +11756,13 @@ function isTrustedOrigin(host) {
11752
11756
  // Domains that Pendo owns, will be swapped in by build patches
11753
11757
  const patterns = [
11754
11758
  /^https:\/\/(adopt\.)?((us1)\.)?(app|via|adopt|cdn|oem|novus-api|novus)(\.(au|eu|gov|hsbc|jpn))?\.pendo\.io$/,
11755
- /^https:\/\/([0-9]{8}t[0-9]{4}-dot-)pendo-(au|eu|govramp|hsbc|io|jp-prod|us1)\.appspot\.com$/,
11756
- /^https:\/\/hotfix-(ops|app)-([0-9]+-dot-)pendo-(au|eu|govramp|hsbc|io|jp-prod|us1)\.appspot\.com$/,
11757
- /^https:\/\/pendo-(au|eu|govramp|hsbc|io|jp-prod|us1)-static\.storage\.googleapis\.com$/,
11759
+ /^https:\/\/([0-9]{8}t[0-9]{4}-dot-)(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-us1)\.appspot\.com$/,
11760
+ /^https:\/\/hotfix-(ops|app)-([0-9]+-dot-)(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-us1)\.appspot\.com$/,
11761
+ /^https:\/\/(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-us1)-static\.storage\.googleapis\.com$/,
11758
11762
  /^https:\/\/([a-zA-Z0-9-]+\.)*pendo-dev\.com$/,
11759
- /^https:\/\/([a-zA-Z0-9-]+-dot-)?pendo-(apollo|armada|atlas|batman|calypso|dap|dev|dr-us-east1|eu-dev|freeze|helix|link|magic|mango|mcfly|ml|mobile-fbi|mobile-guides|mobile-hummus|mobile-plat|perfserf|voc|wildlings|ionchef|scrum-ops|security|test|uat|au|eu|govramp|hsbc|io|jp-prod|us1)\.appspot\.com$/,
11763
+ /^https:\/\/([a-zA-Z0-9-]+-dot-)?(pendo-apollo|pendo-armada|pendo-atlas|pendo-batman|pendo-calypso|pendo-dap|pendo-dev|pendo-dr-us-east1|pendo-eu-dev|pendo-freeze|pendo-helix|pendo-link|pendo-magic|pendo-mango|pendo-mcfly|pendo-ml|pendo-mobile-fbi|pendo-mobile-guides|pendo-mobile-hummus|pendo-mobile-plat|pendo-perfserf|pendo-voc|pendo-wildlings|pendo-ionchef|pendo-scrum-ops|pendo-security|pendo-test|pendo-uat|pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-us1)\.appspot\.com$/,
11760
11764
  /^https:\/\/(via|adopt|local)\.pendo\.(local|io):\d{4}$/,
11761
- /^https:\/\/pendo-(apollo|armada|atlas|batman|calypso|dap|dev|dr-us-east1|eu-dev|freeze|helix|link|magic|mango|mcfly|ml|mobile-fbi|mobile-guides|mobile-hummus|mobile-plat|perfserf|voc|wildlings|ionchef|scrum-ops|security|test|uat)-static\.storage\.googleapis\.com$/
11765
+ /^https:\/\/(pendo-apollo|pendo-armada|pendo-atlas|pendo-batman|pendo-calypso|pendo-dap|pendo-dev|pendo-dr-us-east1|pendo-eu-dev|pendo-freeze|pendo-helix|pendo-link|pendo-magic|pendo-mango|pendo-mcfly|pendo-ml|pendo-mobile-fbi|pendo-mobile-guides|pendo-mobile-hummus|pendo-mobile-plat|pendo-perfserf|pendo-voc|pendo-wildlings|pendo-ionchef|pendo-scrum-ops|pendo-security|pendo-test|pendo-uat)-static\.storage\.googleapis\.com$/
11762
11766
  ]; // eslint-disable-line web-sdk-eslint-rules/no-gulp-env-references
11763
11767
  var adoptHost = ConfigReader.get('adoptHost');
11764
11768
  if (adoptHost) {
@@ -11846,7 +11850,9 @@ var trustedOriginForLoadResource = function (origin) {
11846
11850
  return isTrustedOrigin(origin);
11847
11851
  };
11848
11852
  var parseUrlOrigin = function (url) {
11849
- return new URL(url).origin;
11853
+ // Resolve against the document URL so relative asset urls (used when self-hosting
11854
+ // with an empty asset host) parse to the current page origin instead of throwing.
11855
+ return new URL(url, window.location.href).origin;
11850
11856
  };
11851
11857
  var loadResource = function (options, callback, sendErrorToCallback = false) {
11852
11858
  try {
@@ -20097,6 +20103,7 @@ var GuideStateModule = (function () {
20097
20103
  storageKey: LAST_STEP_ADVANCED_COOKIE,
20098
20104
  guideRequestExpiration: 60 * 60 * 1000,
20099
20105
  scopedStorageKey: null,
20106
+ scopedThrottlingStorageKeys: {},
20100
20107
  receivedStateChangeAt: null,
20101
20108
  receivedLastGuideStepSeen: null,
20102
20109
  lastGuideStepSeen: null,
@@ -20119,6 +20126,11 @@ var GuideStateModule = (function () {
20119
20126
  */
20120
20127
  context.getters.storage().registry.addLocal(LAST_STEP_ADVANCED_COOKIE);
20121
20128
  context.commit('setScopedStorageKey', getPendoCookieKey(context.state.storageKey));
20129
+ var scopedThrottlingStorageKeys = {};
20130
+ _.each(THROTTLING_STATE, function (throttlingStorageKey) {
20131
+ scopedThrottlingStorageKeys[throttlingStorageKey] = getPendoCookieKey(throttlingStorageKey);
20132
+ });
20133
+ context.commit('setScopedThrottlingStorageKeys', scopedThrottlingStorageKeys);
20122
20134
  Events.identify.on(function (evt) {
20123
20135
  if (wasSessionCleared(evt)) {
20124
20136
  removeIdentificationCookies([LAST_STEP_ADVANCED_COOKIE]);
@@ -20157,11 +20169,21 @@ var GuideStateModule = (function () {
20157
20169
  });
20158
20170
  }
20159
20171
  }
20172
+ context.dispatch('loadThrottling');
20173
+ },
20174
+ loadThrottling(context) {
20175
+ const storage = context.getters.storage();
20160
20176
  _.each(THROTTLING_STATE, function (throttlingStorageKey) {
20161
20177
  const value = storage.read(throttlingStorageKey);
20162
20178
  context.dispatch('updateThrottlingState', { name: throttlingStorageKey, value });
20163
20179
  });
20164
20180
  },
20181
+ applyThrottling(context) {
20182
+ _.each(THROTTLING_STATE, function (throttlingStorageKey) {
20183
+ pendo$1[throttlingStorageKey] = applyTimerCache(pendo$1[throttlingStorageKey], context.state[throttlingStorageKey]);
20184
+ context.dispatch('updateThrottlingState', { name: throttlingStorageKey, value: pendo$1[throttlingStorageKey] });
20185
+ });
20186
+ },
20165
20187
  forceExpire(context) {
20166
20188
  _.each(context.state.steps, function (stepState, stepId) {
20167
20189
  context.commit('expireStepState', stepId);
@@ -20237,10 +20259,7 @@ var GuideStateModule = (function () {
20237
20259
  if (mostRecentLastGuideStepSeen) {
20238
20260
  context.dispatch('updateLastGuideStepSeen', _.extend({}, context.state.lastGuideStepSeen, mostRecentLastGuideStepSeen));
20239
20261
  }
20240
- _.each(THROTTLING_STATE, function (throttlingStorageKey) {
20241
- pendo$1[throttlingStorageKey] = applyTimerCache(pendo$1[throttlingStorageKey], context.state[throttlingStorageKey]);
20242
- context.dispatch('updateThrottlingState', { name: throttlingStorageKey, value: pendo$1[throttlingStorageKey] });
20243
- });
20262
+ context.dispatch('applyThrottling');
20244
20263
  },
20245
20264
  receiveLastGuideStepSeen(context, lastGuideStepSeen) {
20246
20265
  context.commit('setReceivedLastGuideStepSeen', lastGuideStepSeen);
@@ -20303,6 +20322,10 @@ var GuideStateModule = (function () {
20303
20322
  }
20304
20323
  }
20305
20324
  }
20325
+ else if (_.contains(_.values(context.state.scopedThrottlingStorageKeys), storageEvent.key)) {
20326
+ context.dispatch('loadThrottling');
20327
+ context.dispatch('applyThrottling');
20328
+ }
20306
20329
  },
20307
20330
  regainFocus(context) {
20308
20331
  var tabLostFocus = context.getters.tabLostFocus();
@@ -20330,6 +20353,9 @@ var GuideStateModule = (function () {
20330
20353
  setScopedStorageKey(state, scopedStorageKey) {
20331
20354
  state.scopedStorageKey = scopedStorageKey;
20332
20355
  },
20356
+ setScopedThrottlingStorageKeys(state, scopedThrottlingStorageKeys) {
20357
+ state.scopedThrottlingStorageKeys = scopedThrottlingStorageKeys;
20358
+ },
20333
20359
  setLastGuideStepSeen(state, lastGuideStepSeen) {
20334
20360
  state.lastGuideStepSeen = lastGuideStepSeen;
20335
20361
  },
@@ -23570,13 +23596,13 @@ var onGuideDismissed = function (evt, step) {
23570
23596
  var seenReason = firstStep && firstStep.seenReason;
23571
23597
  var language = currentGuide && currentGuide.language;
23572
23598
  var dismissCount = _.get(step, 'dismissCount', 0) + 1;
23573
- dismissedGuide(guideId, stepId, get_visitor_id(), seenReason, language, dismissCount);
23574
23599
  var now = getNow();
23575
- _updateGuideStepStatus(guideId, stepId, 'dismissed', now, dismissCount);
23576
- // maintain latestDismissedAutoAt client-side
23600
+ // maintain latestDismissedAutoAt client-side before guideDismissed writes lastStepAdvanced
23577
23601
  if (shouldAffectThrottling(currentGuide, seenReason)) {
23578
23602
  writeThrottlingStateCache(now, THROTTLING_STATE.DISMISSED);
23579
23603
  }
23604
+ dismissedGuide(guideId, stepId, get_visitor_id(), seenReason, language, dismissCount);
23605
+ _updateGuideStepStatus(guideId, stepId, 'dismissed', now, dismissCount);
23580
23606
  step.hide();
23581
23607
  if (!isGuideShown()) {
23582
23608
  stopGuides();
@@ -23632,12 +23658,12 @@ var onGuideSnoozed = function (evt, step, snoozeDuration) {
23632
23658
  log.info('snoozing guide for ' + snoozeDuration + ' ms');
23633
23659
  var snoozeEndTime = now + snoozeDuration;
23634
23660
  step.snoozeEndTime = snoozeEndTime;
23635
- snoozedGuide(guideId, stepId, visitorId, seenReason, language, snoozeDuration);
23636
- _updateGuideStepStatus(guideId, stepId, 'snoozed', now);
23637
- // maintain latestSnoozedAutoAt client-side
23661
+ // maintain latestSnoozedAutoAt client-side before guideSnoozed writes lastStepAdvanced
23638
23662
  if (shouldAffectThrottling(guide, seenReason)) {
23639
23663
  writeThrottlingStateCache(now, THROTTLING_STATE.SNOOZED);
23640
23664
  }
23665
+ snoozedGuide(guideId, stepId, visitorId, seenReason, language, snoozeDuration);
23666
+ _updateGuideStepStatus(guideId, stepId, 'snoozed', now);
23641
23667
  step.hide();
23642
23668
  if (!isGuideShown()) {
23643
23669
  stopGuides();
@@ -23786,15 +23812,15 @@ var onGuideAdvanced = function (evt, step, isIntermediateStep) {
23786
23812
  if (currentGuide && _.last(currentGuide.steps).id === stepId) {
23787
23813
  dismissCount = _.get(step, 'dismissCount', 0) + 1;
23788
23814
  }
23789
- log.info('advancing guide');
23790
- advancedGuide(guideId, stepId, get_visitor_id(), seenReason, language, isIntermediateStep, destinationStepId, dismissCount);
23791
- log.info('update guide status');
23792
23815
  var now = new Date().getTime();
23793
- _updateGuideStepStatus(guideId, stepId, 'advanced', now, dismissCount, destinationStepId);
23794
- // maintain finalAdvancedAutoAt client-side
23816
+ // maintain finalAdvancedAutoAt client-side before guideAdvanced writes lastStepAdvanced
23795
23817
  if (shouldAffectThrottling(currentGuide, seenReason)) {
23796
23818
  writeThrottlingStateCache(now, THROTTLING_STATE.ADVANCED);
23797
23819
  }
23820
+ log.info('advancing guide');
23821
+ advancedGuide(guideId, stepId, get_visitor_id(), seenReason, language, isIntermediateStep, destinationStepId, dismissCount);
23822
+ log.info('update guide status');
23823
+ _updateGuideStepStatus(guideId, stepId, 'advanced', now, dismissCount, destinationStepId);
23798
23824
  log.info('stop guide');
23799
23825
  stopGuides();
23800
23826
  log.info('start guides');
@@ -26035,7 +26061,7 @@ function getWhiteLabelSettings(config) {
26035
26061
  var whiteLabelSettings = ConfigReader.get('whiteLabelSettings');
26036
26062
  if (!whiteLabelSettings && config && config.ux === 'novus') {
26037
26063
  whiteLabelSettings = {
26038
- logoUrl: 'https://novus.pendo.io/novus-square-icon.svg',
26064
+ logoUrl: 'https://novus.pendo.io/branding/novus-vector-white.svg',
26039
26065
  logoStyle: {
26040
26066
  maxWidth: '40px',
26041
26067
  maxHeight: '40px'
@@ -28505,6 +28531,7 @@ const PluginAPI = {
28505
28531
  trim,
28506
28532
  base64EncodeString,
28507
28533
  parseUrl,
28534
+ testPageRule,
28508
28535
  addInlineStyles: _.partial(addInlineStyles, ConfigReader),
28509
28536
  getNow,
28510
28537
  escapeRegExp
@@ -30346,10 +30373,8 @@ function track(name, props, eventProperties) {
30346
30373
  collectEventHelper({ type: 'track', name, props, eventProperties });
30347
30374
  }
30348
30375
 
30349
- const privacyFilterCache = new Map();
30350
30376
  /**
30351
30377
  * Method to manually track agentic events. Uses agent functions for IDs and context.
30352
- * Automatically applies privacy filters if agentId is provided and agent configuration exists.
30353
30378
  *
30354
30379
  * @access public
30355
30380
  * @category Events
@@ -30369,64 +30394,8 @@ const privacyFilterCache = new Map();
30369
30394
  * }, { experiment: 'variant_a' })
30370
30395
  */
30371
30396
  function trackAgent(type, props, eventProperties) {
30372
- const filteredTypes = ['prompt', 'agent_response'];
30373
- if (_.contains(filteredTypes, type)) {
30374
- props = applyPrivacyFilters(props);
30375
- }
30376
30397
  collectEventHelper({ type, name: 'agentic', props, eventProperties });
30377
30398
  }
30378
- /**
30379
- * Apply privacy filters to event properties based on agent configuration
30380
- * Optimized for maximum performance with caching
30381
- * @param {Object} props - Event properties to filter
30382
- * @returns {Object} - Filtered properties
30383
- */
30384
- function applyPrivacyFilters(props) {
30385
- if (!(props === null || props === void 0 ? void 0 : props.agentId)) {
30386
- return props;
30387
- }
30388
- try {
30389
- const agents = ConfigReader.get('aiAgents', []);
30390
- const agent = (agents === null || agents === void 0 ? void 0 : agents.find(agent => agent.id === props.agentId)) || null;
30391
- const content = props.content;
30392
- if (!content) {
30393
- return props;
30394
- }
30395
- if (!(agent === null || agent === void 0 ? void 0 : agent.privacyFilters)) {
30396
- return props;
30397
- }
30398
- const privacyFilter = getCachedRegex(agent.privacyFilters);
30399
- if (!privacyFilter) {
30400
- return props;
30401
- }
30402
- const filteredContent = content.replace(privacyFilter, '<redacted>');
30403
- if (filteredContent === content) {
30404
- return props;
30405
- }
30406
- return Object.assign(Object.assign({}, props), { content: filteredContent });
30407
- }
30408
- catch (error) {
30409
- console.error('Error applying privacy filters', error);
30410
- return props;
30411
- }
30412
- }
30413
- /**
30414
- * Get cached regex or compile and cache new one
30415
- */
30416
- function getCachedRegex(filterPattern) {
30417
- let regex = privacyFilterCache.get(filterPattern);
30418
- if (!regex) {
30419
- try {
30420
- regex = new RegExp(filterPattern, 'gmi');
30421
- privacyFilterCache.set(filterPattern, regex);
30422
- }
30423
- catch (e) {
30424
- privacyFilterCache.set(filterPattern, null);
30425
- return null;
30426
- }
30427
- }
30428
- return regex;
30429
- }
30430
30399
 
30431
30400
  /**
30432
30401
  * Checks visitor and account metadata in the current Pendo installation. Either logs to the console
@@ -37619,7 +37588,7 @@ const GuideCachePlugin = {
37619
37588
  if (!capturedEvent || capturedEvent.type !== 'guideSeen') {
37620
37589
  return;
37621
37590
  }
37622
- if (capturedEvent.props.reason !== 'auto') {
37591
+ if (!_.contains(['auto', 'continue'], capturedEvent.props.reason)) {
37623
37592
  return;
37624
37593
  }
37625
37594
  const snap = getSnapshot(capturedEvent.props.guide_id, capturedEvent.props.guide_step_id);
@@ -40026,7 +39995,7 @@ function SegmentFlags() {
40026
39995
  }
40027
39996
 
40028
39997
  class DOMPrompt {
40029
- constructor(pendo, PluginAPI, id, privacyFilters, inputCssSelectors, submitCssSelectors) {
39998
+ constructor(pendo, PluginAPI, id, inputCssSelectors, submitCssSelectors) {
40030
39999
  this.listeners = [];
40031
40000
  this.latestPromptValue = '';
40032
40001
  this.isActive = true;
@@ -40037,7 +40006,6 @@ class DOMPrompt {
40037
40006
  this.api = PluginAPI;
40038
40007
  this.q = PluginAPI.q;
40039
40008
  this.id = id;
40040
- this.setFilters(privacyFilters);
40041
40009
  if (!this._.isArray(inputCssSelectors)) {
40042
40010
  this.api.log.error('inputCssSelectors must be an array, received:', typeof inputCssSelectors);
40043
40011
  inputCssSelectors = [];
@@ -40140,26 +40108,6 @@ class DOMPrompt {
40140
40108
  const validSubmitEls = this._.filter(this.submitEls, el => el);
40141
40109
  this.promptContainer.addObservers(...validInputEls, ...validSubmitEls);
40142
40110
  }
40143
- setFilters(candidateFilter) {
40144
- if (!candidateFilter) {
40145
- this.privacyFilters = null;
40146
- return null;
40147
- }
40148
- try {
40149
- this.privacyFilters = new RegExp(candidateFilter, 'gmi');
40150
- }
40151
- catch (e) {
40152
- this.privacyFilters = null;
40153
- this.api.log.error(e);
40154
- }
40155
- return this.privacyFilters;
40156
- }
40157
- applyPrivacyFilter(candidateValue, filters = null) {
40158
- const filtersToUse = filters || this.privacyFilters;
40159
- if (!filtersToUse || !this._.isRegExp(filtersToUse))
40160
- return candidateValue;
40161
- return candidateValue.replace(filtersToUse, 'redacted');
40162
- }
40163
40111
  submit(origVal) {
40164
40112
  if (!origVal || !this.isActive) {
40165
40113
  return;
@@ -40170,11 +40118,9 @@ class DOMPrompt {
40170
40118
  return;
40171
40119
  }
40172
40120
  this.lastSubmitTime = now;
40173
- const val = this.applyPrivacyFilter(origVal);
40174
40121
  const payload = {
40175
40122
  agentId: this.id,
40176
- prompt: val,
40177
- privacyFilterApplied: val !== origVal
40123
+ prompt: origVal
40178
40124
  };
40179
40125
  this._.each(this.listeners, (cb) => cb(payload));
40180
40126
  }
@@ -40248,364 +40194,6 @@ class DOMPrompt {
40248
40194
  }
40249
40195
  }
40250
40196
 
40251
- const ASSISTANT_MESSAGE = 'assistantMessage';
40252
- const CONVERSATION_ID_URL_PATTERN = 'conversationIdUrlPattern';
40253
- const MESSAGE_ID_ATTR = 'messageIdAttr';
40254
- const MODEL_USED_ATTR = 'modelUsedAttr';
40255
- const REACTION_ACTIVE = 'reactionActive';
40256
- const STREAMING_ACTIVE = 'streamingActive';
40257
- const STREAMING_ACTIVE_ATTR = 'streamingActiveAttr';
40258
- const THUMB_DOWN = 'thumbDown';
40259
- const THUMB_UP = 'thumbUp';
40260
- const TURN_CONTAINER = 'turnContainer';
40261
- const TURN_ATTR = 'turnAttr';
40262
- const USER_MESSAGE = 'userMessage';
40263
- const CUSTOM_AGENT_ID_URL_PATTERN = 'customAgentIdUrlPattern';
40264
- const CUSTOM_AGENT_NAME = 'customAgentName';
40265
- const REQUIRED_SELECTORS = [
40266
- ASSISTANT_MESSAGE,
40267
- CONVERSATION_ID_URL_PATTERN,
40268
- MESSAGE_ID_ATTR,
40269
- STREAMING_ACTIVE,
40270
- TURN_CONTAINER,
40271
- TURN_ATTR,
40272
- USER_MESSAGE
40273
- ];
40274
- const STREAMING_END_SETTLE_MS = 500;
40275
- const SUBMISSION_START_RETRY_MS = 1000;
40276
- const SUBMISSION_START_MAX_ATTEMPTS = 4;
40277
- class DOMConversation {
40278
- // Returns the list of required selector types that are missing or empty
40279
- // in the given cssSelectors config. An empty array means the config is
40280
- // valid for instantiation.
40281
- static validateConfig(cssSelectors) {
40282
- if (!Array.isArray(cssSelectors)) {
40283
- return REQUIRED_SELECTORS.slice();
40284
- }
40285
- const present = new Set();
40286
- for (const cfg of cssSelectors) {
40287
- if (!cfg || !cfg.type || !cfg.cssSelector)
40288
- continue;
40289
- if (cfg.type === CONVERSATION_ID_URL_PATTERN) {
40290
- try {
40291
- RegExp(cfg.cssSelector);
40292
- }
40293
- catch (e) {
40294
- continue;
40295
- }
40296
- }
40297
- present.add(cfg.type);
40298
- }
40299
- return REQUIRED_SELECTORS.filter((r) => !present.has(r));
40300
- }
40301
- static supportedPreset(preset) {
40302
- return preset === 'chatgpt-full';
40303
- }
40304
- constructor(pendo, PluginAPI, id, privacyFilters, cssSelectors) {
40305
- this.selectors = {};
40306
- this.listeners = [];
40307
- this.isActive = true;
40308
- this.observers = [];
40309
- this.wasStreaming = false;
40310
- this.streamingEndTimer = null;
40311
- this.submissionStartRetryTimer = null;
40312
- this.requestNode = null;
40313
- this.conversationIdRegex = null;
40314
- this.customAgentIdRegex = null;
40315
- this.lastReturnedMessageId = null;
40316
- this.dom = pendo.dom;
40317
- this._ = pendo._;
40318
- this.api = PluginAPI;
40319
- this.Sizzle = pendo.Sizzle;
40320
- this.id = id;
40321
- this.setFilters(privacyFilters);
40322
- if (!this._.isArray(cssSelectors)) {
40323
- this.api.log.error(`cssSelectors must be an array, received: ${typeof cssSelectors}`);
40324
- cssSelectors = [];
40325
- }
40326
- this._.each(cssSelectors, (cfg) => {
40327
- if (cfg && cfg.type) {
40328
- this.selectors[cfg.type] = cfg.cssSelector;
40329
- }
40330
- });
40331
- this.conversationIdRegex = new RegExp(this.selectors[CONVERSATION_ID_URL_PATTERN]);
40332
- const customAgentIdUrlPattern = this.selectors[CUSTOM_AGENT_ID_URL_PATTERN];
40333
- if (customAgentIdUrlPattern) { // this is optional
40334
- try {
40335
- this.customAgentIdRegex = new RegExp(customAgentIdUrlPattern);
40336
- }
40337
- catch (error) {
40338
- this.api.log.error(`bad customAgentIdUrlPattern ${customAgentIdUrlPattern}`, { error });
40339
- }
40340
- }
40341
- this.root = document.body;
40342
- this.setupReactionListener();
40343
- this.setupStreamingObserver();
40344
- }
40345
- setupReactionListener() {
40346
- if (!this.findSelector(THUMB_UP) && !this.findSelector(THUMB_DOWN))
40347
- return;
40348
- this.reactionClickHandler = this.handleReactionClick.bind(this);
40349
- this.root.addEventListener('click', this.reactionClickHandler, true);
40350
- }
40351
- handleReactionClick(evt) {
40352
- const target = evt.target;
40353
- if (!target || target.nodeType !== 1)
40354
- return;
40355
- const upSelector = this.findSelector(THUMB_UP);
40356
- const downSelector = this.findSelector(THUMB_DOWN);
40357
- const thumb = this.dom(target).closest(`${upSelector}, ${downSelector}`);
40358
- if (!thumb.length)
40359
- return;
40360
- const isUp = this.Sizzle.matchesSelector(thumb[0], upSelector);
40361
- const activeSelector = this.findSelector(REACTION_ACTIVE);
40362
- const wasPressed = activeSelector && this.Sizzle.matchesSelector(thumb[0], activeSelector);
40363
- const direction = isUp ? 'positive' : 'negative';
40364
- const content = wasPressed ? 'unreact' : direction;
40365
- const assistantNode = thumb.closest(this.findSelector(TURN_CONTAINER))
40366
- .find(this.findSelector(ASSISTANT_MESSAGE));
40367
- this.emit('user_reaction', {
40368
- agentId: this.id,
40369
- messageId: this.getMessageId(assistantNode),
40370
- content,
40371
- privacyFilterApplied: false
40372
- });
40373
- }
40374
- // observe the streaming indicator
40375
- setupStreamingObserver() {
40376
- this.wasStreaming = this.select(STREAMING_ACTIVE).length > 0;
40377
- this.stopStreamingEndTimer();
40378
- const MutationObserver = getZoneSafeMethod('MutationObserver');
40379
- const observer = new MutationObserver(this.handleMutation.bind(this));
40380
- const attrName = this.findSelector(STREAMING_ACTIVE_ATTR);
40381
- observer.observe(this.root, {
40382
- childList: true,
40383
- subtree: true,
40384
- attributes: true,
40385
- attributeFilter: attrName ? [attrName] : undefined
40386
- });
40387
- this.observers.push(observer);
40388
- }
40389
- // look for changes in the state of the streaming indicator.
40390
- // streaming started -> a prompt has been submitted
40391
- // streaming ended -> got a response
40392
- // we don't send the prompt immediately when it's submitted because it might not have a
40393
- // conversation id yet.
40394
- // we don't send the agent_response immediately when the streaming indicator is cleared
40395
- // because it clears slightly before the final text chunk is committed to the DOM so the
40396
- // end-handling is deferred by a short settle window
40397
- handleMutation() {
40398
- const isStreaming = this.select(STREAMING_ACTIVE).length > 0;
40399
- if (isStreaming === this.wasStreaming)
40400
- return;
40401
- if (isStreaming) { // started streaming
40402
- if (!this.streamingEndTimer) {
40403
- this.onSubmissionStart();
40404
- }
40405
- else {
40406
- // stream restarted during the settle window (multi-phase response).
40407
- // treat as a continuation of the prior request, not a new submission.
40408
- // we'll handle it next time streaming stops
40409
- this.stopStreamingEndTimer();
40410
- }
40411
- }
40412
- else { // finished streaming
40413
- this.startStreamingEndTimer();
40414
- }
40415
- this.wasStreaming = isStreaming;
40416
- }
40417
- onSubmissionStart(attempt = 1) {
40418
- this.requestNode = this.findLastRequestNode();
40419
- if (this.requestNode) {
40420
- this.handleUserMessage(this.requestNode);
40421
- return;
40422
- }
40423
- // URL hasn't flipped yet to include the conversation id on a fresh chat
40424
- // retry, if that still fails, onSubmissionEnd handles it
40425
- if (attempt < SUBMISSION_START_MAX_ATTEMPTS) {
40426
- this.startSubmissionStartRetryTimer(attempt + 1);
40427
- }
40428
- }
40429
- onSubmissionEnd() {
40430
- this.stopSubmissionStartRetryTimer();
40431
- let requestNode = this.requestNode;
40432
- this.requestNode = null;
40433
- if (!requestNode) {
40434
- // onSubmissionStart didn't send, send it now
40435
- requestNode = this.findLastRequestNode();
40436
- if (!requestNode)
40437
- return;
40438
- this.handleUserMessage(requestNode);
40439
- }
40440
- const requestTurn = this.findTurnNumber(requestNode);
40441
- if (requestTurn === null)
40442
- return;
40443
- const assistantNodes = this.select(ASSISTANT_MESSAGE);
40444
- const toEmit = [];
40445
- for (let i = assistantNodes.length - 1; i >= 0; i--) {
40446
- const n = this.findTurnNumber(this.dom(assistantNodes[i]));
40447
- if (n === null)
40448
- continue;
40449
- if (n <= requestTurn)
40450
- break;
40451
- toEmit.push(assistantNodes[i]);
40452
- }
40453
- // emit in chronological order (reverse of collection order)
40454
- for (let i = toEmit.length - 1; i >= 0; i--) {
40455
- this.handleAssistantMessage(this.dom(toEmit[i]));
40456
- }
40457
- }
40458
- extractContent(node) {
40459
- const rawContent = node.text().trim();
40460
- const content = this.applyPrivacyFilter(rawContent);
40461
- return { content, privacyFilterApplied: content !== rawContent };
40462
- }
40463
- getMessageId(node) {
40464
- return node.attr(this.findSelector(MESSAGE_ID_ATTR));
40465
- }
40466
- handleUserMessage(node) {
40467
- this.emit('prompt', Object.assign({ agentId: this.id, messageId: this.getMessageId(node) }, this.extractContent(node)));
40468
- }
40469
- handleAssistantMessage(node) {
40470
- const modelUsedAttr = this.findSelector(MODEL_USED_ATTR);
40471
- const modelUsed = modelUsedAttr && node.attr(modelUsedAttr);
40472
- this.emit('agent_response', Object.assign({ agentId: this.id, messageId: this.getMessageId(node), agentModelsUsed: modelUsed ? [modelUsed] : [] }, this.extractContent(node)));
40473
- }
40474
- findLastRequestNode() {
40475
- if (!this.getConversationId())
40476
- return null;
40477
- const node = this._.last(this.select(USER_MESSAGE));
40478
- if (!node)
40479
- return null;
40480
- const $node = this.dom(node);
40481
- const messageId = this.getMessageId($node);
40482
- if (!messageId)
40483
- return null;
40484
- if (messageId === this.lastReturnedMessageId)
40485
- return null;
40486
- this.lastReturnedMessageId = messageId;
40487
- return $node;
40488
- }
40489
- findTurnNumber(node) {
40490
- const turnSelector = this.findSelector(TURN_CONTAINER);
40491
- const turnAttr = this.findSelector(TURN_ATTR);
40492
- const turn = node.closest(turnSelector).attr(turnAttr);
40493
- if (!turn)
40494
- return null;
40495
- const seqMatch = String(turn).match(/\d+/);
40496
- return seqMatch ? parseInt(seqMatch[0], 10) : null;
40497
- }
40498
- findSelector(type) {
40499
- return this.selectors[type];
40500
- }
40501
- select(type) {
40502
- return this.dom(this.findSelector(type), this.root);
40503
- }
40504
- getConversationId() {
40505
- const m = location.href.match(this.conversationIdRegex);
40506
- if (!m)
40507
- return null;
40508
- return m[1] !== undefined ? m[1] : m[0];
40509
- }
40510
- getCustomAgentId() {
40511
- if (!this.customAgentIdRegex)
40512
- return undefined;
40513
- const m = location.href.match(this.customAgentIdRegex);
40514
- if (!m)
40515
- return undefined;
40516
- return m[1] !== undefined ? m[1] : m[0];
40517
- }
40518
- getCustomAgentName() {
40519
- if (!this.findSelector(CUSTOM_AGENT_NAME))
40520
- return '';
40521
- const node = this.select(CUSTOM_AGENT_NAME);
40522
- if (!node.length)
40523
- return '';
40524
- return node.text().trim();
40525
- }
40526
- addCustomAgentInfo(eventPayload) {
40527
- const customAgentId = this.getCustomAgentId();
40528
- if (!customAgentId)
40529
- return;
40530
- eventPayload.customAgentId = customAgentId;
40531
- const customAgentName = this.getCustomAgentName();
40532
- if (customAgentName) {
40533
- eventPayload.customAgentName = customAgentName;
40534
- }
40535
- }
40536
- setFilters(candidateFilter) {
40537
- if (!candidateFilter) {
40538
- this.privacyFilters = null;
40539
- return null;
40540
- }
40541
- try {
40542
- this.privacyFilters = new RegExp(candidateFilter, 'gmi');
40543
- }
40544
- catch (e) {
40545
- this.privacyFilters = null;
40546
- this.api.log.error(e);
40547
- }
40548
- return this.privacyFilters;
40549
- }
40550
- applyPrivacyFilter(candidateValue, filters = null) {
40551
- const filtersToUse = filters || this.privacyFilters;
40552
- if (!filtersToUse || !this._.isRegExp(filtersToUse))
40553
- return candidateValue;
40554
- return candidateValue.replace(filtersToUse, 'redacted');
40555
- }
40556
- onSubmit(callback) {
40557
- this.listeners.push(callback);
40558
- }
40559
- emit(eventType, eventPayload) {
40560
- eventPayload.conversationId = this.getConversationId();
40561
- this.addCustomAgentInfo(eventPayload);
40562
- this._.each(this.listeners, (cb) => cb(eventType, eventPayload));
40563
- }
40564
- startStreamingEndTimer() {
40565
- this.stopStreamingEndTimer();
40566
- this.streamingEndTimer = setTimeout$1(() => {
40567
- this.streamingEndTimer = null;
40568
- this.onSubmissionEnd();
40569
- }, STREAMING_END_SETTLE_MS);
40570
- }
40571
- stopStreamingEndTimer() {
40572
- if (!this.streamingEndTimer)
40573
- return;
40574
- clearTimeout(this.streamingEndTimer);
40575
- this.streamingEndTimer = null;
40576
- }
40577
- startSubmissionStartRetryTimer(attempt) {
40578
- this.stopSubmissionStartRetryTimer();
40579
- this.submissionStartRetryTimer = setTimeout$1(() => {
40580
- this.submissionStartRetryTimer = null;
40581
- this.onSubmissionStart(attempt);
40582
- }, SUBMISSION_START_RETRY_MS);
40583
- }
40584
- stopSubmissionStartRetryTimer() {
40585
- if (!this.submissionStartRetryTimer)
40586
- return;
40587
- clearTimeout(this.submissionStartRetryTimer);
40588
- this.submissionStartRetryTimer = null;
40589
- }
40590
- suspend() {
40591
- this.isActive = false;
40592
- }
40593
- resume() {
40594
- this.isActive = true;
40595
- }
40596
- teardown() {
40597
- this._.each(this.observers, (o) => o && o.disconnect && o.disconnect());
40598
- this.observers = [];
40599
- if (this.reactionClickHandler && this.root) {
40600
- this.root.removeEventListener('click', this.reactionClickHandler, true);
40601
- this.reactionClickHandler = null;
40602
- }
40603
- this.stopStreamingEndTimer();
40604
- this.stopSubmissionStartRetryTimer();
40605
- this.listeners = [];
40606
- }
40607
- }
40608
-
40609
40197
  /*
40610
40198
  * Prompt Analytics Plugin
40611
40199
  *
@@ -40690,6 +40278,9 @@ class PromptPlugin {
40690
40278
  });
40691
40279
  const currentlyActiveIds = this._.map(this._.filter(this.prompts, prompt => prompt.isActive), prompt => prompt.id);
40692
40280
  this._.each(allAgents, aiAgent => {
40281
+ if (this.isConversationAgent(aiAgent)) {
40282
+ return;
40283
+ }
40693
40284
  const isCurrentlyActive = this._.contains(currentlyActiveIds, aiAgent.id);
40694
40285
  const shouldBeActive = this.testPageRule(aiAgent.pageRule, currentUrl, aiAgent.id);
40695
40286
  if (shouldBeActive && !isCurrentlyActive) {
@@ -40703,10 +40294,9 @@ class PromptPlugin {
40703
40294
  }
40704
40295
  return this.agentsCache.get(agentId);
40705
40296
  }
40706
- applyPrivacyFilter(candidateValue, filters = null) {
40707
- if (!filters || !this._.isRegExp(filters))
40708
- return candidateValue;
40709
- return candidateValue.replace(filters, 'redacted');
40297
+ // conversation agents, owned by the ConversationAnalytics plugin.
40298
+ isConversationAgent(agent) {
40299
+ return agent.preset && agent.preset != 'chatgpt';
40710
40300
  }
40711
40301
  onConfigLoaded(aiAgents = []) {
40712
40302
  if (!aiAgents || !this._.isArray(aiAgents) || aiAgents.length === 0) {
@@ -40715,6 +40305,9 @@ class PromptPlugin {
40715
40305
  const networkAgents = [];
40716
40306
  const domAgents = [];
40717
40307
  for (const agent of aiAgents) {
40308
+ if (this.isConversationAgent(agent)) {
40309
+ continue;
40310
+ }
40718
40311
  if (agent.trackingMethod === 'network') {
40719
40312
  networkAgents.push(agent);
40720
40313
  }
@@ -40751,13 +40344,11 @@ class PromptPlugin {
40751
40344
  return;
40752
40345
  }
40753
40346
  const originalBody = body || '';
40754
- const filteredPrompt = this.applyPrivacyFilter(originalBody, aiAgent.privacyFilters);
40755
40347
  this.api.analytics.collectEvent('prompt', {
40756
- prompt: filteredPrompt,
40348
+ prompt: originalBody,
40757
40349
  agentId: aiAgent.id,
40758
40350
  promptType: method,
40759
- url,
40760
- privacyFilterApplied: filteredPrompt !== originalBody
40351
+ url
40761
40352
  }, undefined, 'agentic');
40762
40353
  });
40763
40354
  }
@@ -40770,29 +40361,16 @@ class PromptPlugin {
40770
40361
  }
40771
40362
  }
40772
40363
  observePrompt(config) {
40773
- const { id, cssSelectors, privacyFilters, preset } = config;
40364
+ const { id, cssSelectors } = config;
40774
40365
  // Check if agent already exists
40775
40366
  const existingPrompt = this._.find(this.prompts, prompt => prompt.id === id);
40776
40367
  if (existingPrompt) {
40777
40368
  return;
40778
40369
  }
40779
- if (DOMConversation.supportedPreset(preset)) {
40780
- const missing = DOMConversation.validateConfig(cssSelectors);
40781
- if (missing.length) {
40782
- this.api.log.error('aiAgent config missing required selectors', { agentId: id, missing });
40783
- return;
40784
- }
40785
- const conversation = new DOMConversation(this.pendo, this.api, id, privacyFilters, cssSelectors);
40786
- conversation.onSubmit((eventType, payload) => {
40787
- this.sendConversationEvent(eventType, payload);
40788
- });
40789
- this.prompts.push(conversation);
40790
- return;
40791
- }
40792
40370
  let inputCssSelectors, submitCssSelectors;
40793
40371
  inputCssSelectors = this.parseCssSelector('input', cssSelectors);
40794
40372
  submitCssSelectors = this.parseCssSelector('submit', cssSelectors);
40795
- const prompt = new DOMPrompt(this.pendo, this.api, id, privacyFilters, inputCssSelectors, submitCssSelectors);
40373
+ const prompt = new DOMPrompt(this.pendo, this.api, id, inputCssSelectors, submitCssSelectors);
40796
40374
  prompt.onSubmit((promptPayload) => {
40797
40375
  this.sendPromptEvent(promptPayload);
40798
40376
  });
@@ -40815,15 +40393,6 @@ class PromptPlugin {
40815
40393
  }, promptEvent);
40816
40394
  this.api.analytics.collectEvent('prompt', event, undefined, 'agentic');
40817
40395
  }
40818
- sendConversationEvent(eventType, eventPayload) {
40819
- // Block event if the prompt is suspended
40820
- const conv = this._.find(this.prompts, p => p.id === eventPayload.agentId);
40821
- if (conv && !conv.isActive) {
40822
- return;
40823
- }
40824
- eventPayload = this._.extend({ agentType: 'conversation' }, eventPayload);
40825
- this.api.analytics.collectEvent(eventType, eventPayload, undefined, 'agentic');
40826
- }
40827
40396
  teardown() {
40828
40397
  this._.each(this.prompts, (prompt) => prompt.teardown());
40829
40398
  if (this.agentsCache) {
@@ -41210,10 +40779,7 @@ class WebAnalytics {
41210
40779
  PluginAPI.attachEvent(PluginAPI.Events, 'eventCaptured', bind(this.addUtmToEvent, this)),
41211
40780
  PluginAPI.attachEvent(PluginAPI.Events, 'sessionChanged', bind(this.sessionChanged, this))
41212
40781
  ];
41213
- let params = this.loadParameters(PluginAPI.agentStorage);
41214
- if (!params || pendo._.isEmpty(params)) {
41215
- params = this.extractParameters(window.location.href, document.referrer);
41216
- }
40782
+ this.suffix = this.api.ConfigReader.get('identityStorageSuffix');
41217
40783
  /**
41218
40784
  * Stores UTM parameters that were present in the URL when the visitor loaded the application.
41219
40785
  *
@@ -41222,9 +40788,12 @@ class WebAnalytics {
41222
40788
  * @access public
41223
40789
  * @label WEB_ANALYTICS_STORAGE_KEY
41224
40790
  */
41225
- this.api.agentStorage.registry.addLocal(WEB_ANALYTICS_STORAGE_KEY);
40791
+ this.api.agentStorage.registry.addLocal(WEB_ANALYTICS_STORAGE_KEY, { cookieSuffix: this.suffix, isSecure: true });
40792
+ let params = this.loadParameters(PluginAPI.agentStorage);
40793
+ if (!params || pendo._.isEmpty(params)) {
40794
+ params = this.extractParameters(window.location.href, document.referrer);
40795
+ }
41226
40796
  this.storeParameters(params, PluginAPI.agentStorage);
41227
- this.suffix = this.api.ConfigReader.get('identityStorageSuffix');
41228
40797
  }
41229
40798
  extractParameters(url, referrer) {
41230
40799
  const locationUrl = new URL(url);
@@ -41279,12 +40848,12 @@ class WebAnalytics {
41279
40848
  }
41280
40849
  storeParameters(params, storage) {
41281
40850
  if (this.pendo._.size(params) > 0) {
41282
- storage.write(WEB_ANALYTICS_STORAGE_KEY, JSON.stringify(params), undefined, false, true, this.suffix);
40851
+ storage.write(WEB_ANALYTICS_STORAGE_KEY, JSON.stringify(params));
41283
40852
  }
41284
40853
  }
41285
40854
  loadParameters(storage) {
41286
- const storedParamsJson = storage.read(WEB_ANALYTICS_STORAGE_KEY, false, this.suffix) ||
41287
- storage.read(WEB_ANALYTICS_STORAGE_KEY);
40855
+ const storedParamsJson = storage.read(WEB_ANALYTICS_STORAGE_KEY) ||
40856
+ storage.read(WEB_ANALYTICS_STORAGE_KEY, false, '');
41288
40857
  if (storedParamsJson)
41289
40858
  return JSON.parse(storedParamsJson);
41290
40859
  return null;
@@ -41310,7 +40879,7 @@ class WebAnalytics {
41310
40879
  sessionChanged() {
41311
40880
  const agentStorage = this.api.agentStorage;
41312
40881
  agentStorage.clear(WEB_ANALYTICS_STORAGE_KEY);
41313
- agentStorage.clear(WEB_ANALYTICS_STORAGE_KEY, false, this.suffix);
40882
+ agentStorage.clear(WEB_ANALYTICS_STORAGE_KEY, false, '');
41314
40883
  const params = this.extractParameters(window.location.href, document.referrer);
41315
40884
  this.storeParameters(params, agentStorage);
41316
40885
  }
@@ -41805,6 +41374,21 @@ function branchingGoToStep(event, step, guide, pendo) {
41805
41374
  event.cancel = true;
41806
41375
  }
41807
41376
 
41377
+ var DANGEROUS_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
41378
+ function encodeMetadataForUrl(value, urlBeforePlaceholder) {
41379
+ if (urlBeforePlaceholder === void 0) { urlBeforePlaceholder = ''; }
41380
+ var str = value === undefined || value === null ? '' : String(value);
41381
+ var inUrlComponent = urlBeforePlaceholder.indexOf('?') !== -1 || urlBeforePlaceholder.indexOf('#') !== -1;
41382
+ if (inUrlComponent) {
41383
+ return window.encodeURIComponent(str);
41384
+ }
41385
+ var schemeAlreadySet = urlBeforePlaceholder.indexOf(':') !== -1;
41386
+ if (!schemeAlreadySet && DANGEROUS_URL_SCHEME.test(str)) {
41387
+ return '';
41388
+ }
41389
+ return window.encodeURI(str);
41390
+ }
41391
+
41808
41392
  const MetadataSubstitution = {
41809
41393
  name: 'MetadataSubstitution',
41810
41394
  script(step, guide, pendo) {
@@ -41948,8 +41532,10 @@ function substituteMetadataByTarget(data, target, mdValue, matched) {
41948
41532
  ? (data.getAttribute(target) || '')
41949
41533
  : data[target];
41950
41534
  if (target === 'href' || target === 'value') {
41951
- const safeValue = window.encodeURIComponent(String(mdValue === undefined || mdValue === null ? '' : mdValue));
41952
- data[target] = decodeURI(current).replace(matched[0], safeValue);
41535
+ const decodedUrl = decodeURI(current);
41536
+ const urlBeforePlaceholder = decodedUrl.slice(0, decodedUrl.indexOf(matched[0]));
41537
+ const safeValue = encodeMetadataForUrl(mdValue, urlBeforePlaceholder);
41538
+ data[target] = decodedUrl.replace(matched[0], safeValue);
41953
41539
  }
41954
41540
  else {
41955
41541
  data[target] = current.replace(matched[0], mdValue);
@@ -53384,4 +52970,478 @@ const PredictGuides = () => {
53384
52970
  };
53385
52971
  };
53386
52972
 
53387
- export { ConsoleCapture, Feedback, GuideMarkdown, NetworkCapture, PredictGuides, createReplayPlugin as Replay, TextCapture, VocPortal, loadAgent };
52973
+ const ASSISTANT_MESSAGE = 'assistantMessage';
52974
+ const CONVERSATION_ID_URL_PATTERN = 'conversationIdUrlPattern';
52975
+ const MESSAGE_ID_ATTR = 'messageIdAttr';
52976
+ const MESSAGE_ID_PATTERN = 'messageIdPattern';
52977
+ const MODEL_USED_ATTR = 'modelUsedAttr';
52978
+ const REACTION_ACTIVE = 'reactionActive';
52979
+ const STREAMING_ACTIVE = 'streamingActive';
52980
+ const STREAMING_ACTIVE_ATTR = 'streamingActiveAttr';
52981
+ const THUMB_DOWN = 'thumbDown';
52982
+ const THUMB_UP = 'thumbUp';
52983
+ const TURN_CONTAINER = 'turnContainer';
52984
+ const USER_MESSAGE = 'userMessage';
52985
+ const CUSTOM_AGENT_ID_URL_PATTERN = 'customAgentIdUrlPattern';
52986
+ const CUSTOM_AGENT_NAME = 'customAgentName';
52987
+ const REQUIRED_SELECTORS = [
52988
+ ASSISTANT_MESSAGE,
52989
+ CONVERSATION_ID_URL_PATTERN,
52990
+ MESSAGE_ID_ATTR,
52991
+ STREAMING_ACTIVE,
52992
+ TURN_CONTAINER,
52993
+ USER_MESSAGE
52994
+ ];
52995
+ const SUPPORTED_PRESETS = ['chatgpt-full', 'gemini-full'];
52996
+ const STREAMING_END_SETTLE_MS = 500;
52997
+ const SUBMISSION_START_RETRY_MS = 1000;
52998
+ const SUBMISSION_START_MAX_ATTEMPTS = 4;
52999
+ class DOMConversation {
53000
+ // Returns the list of required selector types that are missing or empty
53001
+ // in the given cssSelectors config. An empty array means the config is
53002
+ // valid for instantiation.
53003
+ static validateConfig(cssSelectors) {
53004
+ if (!Array.isArray(cssSelectors)) {
53005
+ return REQUIRED_SELECTORS.slice();
53006
+ }
53007
+ const present = new Set();
53008
+ for (const cfg of cssSelectors) {
53009
+ if (!cfg || !cfg.type || !cfg.cssSelector)
53010
+ continue;
53011
+ if (cfg.type === CONVERSATION_ID_URL_PATTERN) {
53012
+ try {
53013
+ RegExp(cfg.cssSelector);
53014
+ }
53015
+ catch (e) {
53016
+ continue;
53017
+ }
53018
+ }
53019
+ present.add(cfg.type);
53020
+ }
53021
+ return REQUIRED_SELECTORS.filter((r) => !present.has(r));
53022
+ }
53023
+ static supportedPreset(preset) {
53024
+ return SUPPORTED_PRESETS.indexOf(preset) !== -1;
53025
+ }
53026
+ constructor(pendo, PluginAPI, id, cssSelectors) {
53027
+ this.selectors = {};
53028
+ this.listeners = [];
53029
+ this.isActive = true;
53030
+ this.observers = [];
53031
+ this.wasStreaming = false;
53032
+ this.streamingEndTimer = null;
53033
+ this.submissionStartRetryTimer = null;
53034
+ this.requestNode = null;
53035
+ this.conversationIdRegex = null;
53036
+ this.customAgentIdRegex = null;
53037
+ this.messageIdRegex = null;
53038
+ this.lastReturnedMessageId = null;
53039
+ this.dom = pendo.dom;
53040
+ this._ = pendo._;
53041
+ this.api = PluginAPI;
53042
+ this.Sizzle = pendo.Sizzle;
53043
+ this.id = id;
53044
+ if (!this._.isArray(cssSelectors)) {
53045
+ this.api.log.error(`cssSelectors must be an array, received: ${typeof cssSelectors}`);
53046
+ cssSelectors = [];
53047
+ }
53048
+ this._.each(cssSelectors, (cfg) => {
53049
+ if (cfg && cfg.type) {
53050
+ this.selectors[cfg.type] = cfg.cssSelector;
53051
+ }
53052
+ });
53053
+ this.conversationIdRegex = new RegExp(this.selectors[CONVERSATION_ID_URL_PATTERN]);
53054
+ const customAgentIdUrlPattern = this.selectors[CUSTOM_AGENT_ID_URL_PATTERN];
53055
+ if (customAgentIdUrlPattern) { // this is optional
53056
+ try {
53057
+ this.customAgentIdRegex = new RegExp(customAgentIdUrlPattern);
53058
+ }
53059
+ catch (error) {
53060
+ this.api.log.error(`bad customAgentIdUrlPattern ${customAgentIdUrlPattern}`, { error });
53061
+ }
53062
+ }
53063
+ const messageIdPattern = this.selectors[MESSAGE_ID_PATTERN];
53064
+ if (messageIdPattern) { // optional: gates the request node until its id is fully rendered
53065
+ try {
53066
+ this.messageIdRegex = new RegExp(messageIdPattern);
53067
+ }
53068
+ catch (error) {
53069
+ this.api.log.error(`bad messageIdPattern ${messageIdPattern}`, { error });
53070
+ }
53071
+ }
53072
+ this.root = document.body;
53073
+ this.setupReactionListener();
53074
+ this.setupStreamingObserver();
53075
+ }
53076
+ setupReactionListener() {
53077
+ if (!this.findSelector(THUMB_UP) && !this.findSelector(THUMB_DOWN))
53078
+ return;
53079
+ this.reactionClickHandler = this.handleReactionClick.bind(this);
53080
+ this.root.addEventListener('click', this.reactionClickHandler, true);
53081
+ }
53082
+ handleReactionClick(evt) {
53083
+ const target = evt.target;
53084
+ if (!target || target.nodeType !== 1)
53085
+ return;
53086
+ const upSelector = this.findSelector(THUMB_UP);
53087
+ const downSelector = this.findSelector(THUMB_DOWN);
53088
+ const thumb = this.dom(target).closest(`${upSelector}, ${downSelector}`);
53089
+ if (!thumb.length)
53090
+ return;
53091
+ const isUp = this.Sizzle.matchesSelector(thumb[0], upSelector);
53092
+ const activeSelector = this.findSelector(REACTION_ACTIVE);
53093
+ const wasPressed = activeSelector && this.Sizzle.matchesSelector(thumb[0], activeSelector);
53094
+ const direction = isUp ? 'positive' : 'negative';
53095
+ const content = wasPressed ? 'unreact' : direction;
53096
+ const assistantNode = thumb.closest(this.findSelector(TURN_CONTAINER))
53097
+ .find(this.findSelector(ASSISTANT_MESSAGE));
53098
+ this.emit('user_reaction', {
53099
+ agentId: this.id,
53100
+ messageId: this.getMessageId(assistantNode),
53101
+ content
53102
+ });
53103
+ }
53104
+ // observe the streaming indicator
53105
+ setupStreamingObserver() {
53106
+ this.wasStreaming = this.select(STREAMING_ACTIVE).length > 0;
53107
+ this.stopStreamingEndTimer();
53108
+ const MutationObserver = getZoneSafeMethod(this._, 'MutationObserver');
53109
+ const observer = new MutationObserver(this.handleMutation.bind(this));
53110
+ const attrName = this.findSelector(STREAMING_ACTIVE_ATTR);
53111
+ observer.observe(this.root, {
53112
+ childList: true,
53113
+ subtree: true,
53114
+ attributes: true,
53115
+ attributeFilter: attrName ? [attrName] : undefined
53116
+ });
53117
+ this.observers.push(observer);
53118
+ }
53119
+ // look for changes in the state of the streaming indicator.
53120
+ // streaming started -> a prompt has been submitted
53121
+ // streaming ended -> got a response
53122
+ // we don't send the prompt immediately when it's submitted because it might not have a
53123
+ // conversation id yet.
53124
+ // we don't send the agent_response immediately when the streaming indicator is cleared
53125
+ // because it clears slightly before the final text chunk is committed to the DOM so the
53126
+ // end-handling is deferred by a short settle window
53127
+ handleMutation() {
53128
+ const isStreaming = this.select(STREAMING_ACTIVE).length > 0;
53129
+ if (isStreaming === this.wasStreaming)
53130
+ return;
53131
+ if (isStreaming) { // started streaming
53132
+ if (!this.streamingEndTimer) {
53133
+ this.onSubmissionStart();
53134
+ }
53135
+ else {
53136
+ // stream restarted during the settle window (multi-phase response).
53137
+ // treat as a continuation of the prior request, not a new submission.
53138
+ // we'll handle it next time streaming stops
53139
+ this.stopStreamingEndTimer();
53140
+ }
53141
+ }
53142
+ else { // finished streaming
53143
+ this.startStreamingEndTimer();
53144
+ }
53145
+ this.wasStreaming = isStreaming;
53146
+ }
53147
+ onSubmissionStart(attempt = 1) {
53148
+ this.requestNode = this.findLastRequestNode();
53149
+ if (this.requestNode) {
53150
+ this.handleUserMessage(this.requestNode);
53151
+ return;
53152
+ }
53153
+ // URL hasn't flipped yet to include the conversation id on a fresh chat
53154
+ // retry, if that still fails, onSubmissionEnd handles it
53155
+ if (attempt < SUBMISSION_START_MAX_ATTEMPTS) {
53156
+ this.startSubmissionStartRetryTimer(attempt + 1);
53157
+ }
53158
+ }
53159
+ onSubmissionEnd() {
53160
+ this.stopSubmissionStartRetryTimer();
53161
+ let requestNode = this.requestNode;
53162
+ this.requestNode = null;
53163
+ if (!requestNode) {
53164
+ // onSubmissionStart didn't send, send it now
53165
+ requestNode = this.findLastRequestNode();
53166
+ if (!requestNode)
53167
+ return;
53168
+ this.handleUserMessage(requestNode);
53169
+ }
53170
+ const requestEl = requestNode[0];
53171
+ if (!requestEl)
53172
+ return;
53173
+ // the request is the latest user message, so any assistant message that
53174
+ // follows it in the DOM is a response to it. document order is already
53175
+ // chronological, so we emit as we go
53176
+ const assistantNodes = this.select(ASSISTANT_MESSAGE);
53177
+ for (let i = 0; i < assistantNodes.length; i++) {
53178
+ const el = assistantNodes[i];
53179
+ if (requestEl.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING) {
53180
+ this.handleAssistantMessage(this.dom(el));
53181
+ }
53182
+ }
53183
+ }
53184
+ extractContent(node) {
53185
+ return { content: node.text().trim() };
53186
+ }
53187
+ getMessageId(node) {
53188
+ return node.attr(this.findSelector(MESSAGE_ID_ATTR));
53189
+ }
53190
+ handleUserMessage(node) {
53191
+ this.emit('prompt', Object.assign({ agentId: this.id, messageId: this.getMessageId(node) }, this.extractContent(node)));
53192
+ }
53193
+ handleAssistantMessage(node) {
53194
+ const modelUsedAttr = this.findSelector(MODEL_USED_ATTR);
53195
+ const modelUsed = modelUsedAttr && node.attr(modelUsedAttr);
53196
+ this.emit('agent_response', Object.assign({ agentId: this.id, messageId: this.getMessageId(node), agentModelsUsed: modelUsed ? [modelUsed] : [] }, this.extractContent(node)));
53197
+ }
53198
+ findLastRequestNode() {
53199
+ if (!this.getConversationId())
53200
+ return null;
53201
+ const node = this._.last(this.select(USER_MESSAGE));
53202
+ if (!node)
53203
+ return null;
53204
+ const $node = this.dom(node);
53205
+ const messageId = this.getMessageId($node);
53206
+ if (!messageId)
53207
+ return null;
53208
+ // the node may still be rendering (e.g. a framework interpolating an
53209
+ // unresolved id); treat it as not-ready so the retry waits for the
53210
+ // finalized node rather than caching a placeholder that gets replaced
53211
+ if (this.messageIdRegex && !this.messageIdRegex.test(messageId))
53212
+ return null;
53213
+ if (messageId === this.lastReturnedMessageId)
53214
+ return null;
53215
+ this.lastReturnedMessageId = messageId;
53216
+ return $node;
53217
+ }
53218
+ findSelector(type) {
53219
+ return this.selectors[type];
53220
+ }
53221
+ select(type) {
53222
+ return this.dom(this.findSelector(type), this.root);
53223
+ }
53224
+ getConversationId() {
53225
+ const m = location.href.match(this.conversationIdRegex);
53226
+ if (!m)
53227
+ return null;
53228
+ return m[1] !== undefined ? m[1] : m[0];
53229
+ }
53230
+ getCustomAgentId() {
53231
+ if (!this.customAgentIdRegex)
53232
+ return undefined;
53233
+ const m = location.href.match(this.customAgentIdRegex);
53234
+ if (!m)
53235
+ return undefined;
53236
+ return m[1] !== undefined ? m[1] : m[0];
53237
+ }
53238
+ getCustomAgentName() {
53239
+ if (!this.findSelector(CUSTOM_AGENT_NAME))
53240
+ return '';
53241
+ const node = this.select(CUSTOM_AGENT_NAME);
53242
+ if (!node.length)
53243
+ return '';
53244
+ return node.text().trim();
53245
+ }
53246
+ addCustomAgentInfo(eventPayload) {
53247
+ const customAgentId = this.getCustomAgentId();
53248
+ if (!customAgentId)
53249
+ return;
53250
+ eventPayload.customAgentId = customAgentId;
53251
+ const customAgentName = this.getCustomAgentName();
53252
+ if (customAgentName) {
53253
+ eventPayload.customAgentName = customAgentName;
53254
+ }
53255
+ }
53256
+ onSubmit(callback) {
53257
+ this.listeners.push(callback);
53258
+ }
53259
+ emit(eventType, eventPayload) {
53260
+ eventPayload.conversationId = this.getConversationId();
53261
+ this.addCustomAgentInfo(eventPayload);
53262
+ this._.each(this.listeners, (cb) => cb(eventType, eventPayload));
53263
+ }
53264
+ startStreamingEndTimer() {
53265
+ this.stopStreamingEndTimer();
53266
+ this.streamingEndTimer = setTimeout$1(() => {
53267
+ this.streamingEndTimer = null;
53268
+ this.onSubmissionEnd();
53269
+ }, STREAMING_END_SETTLE_MS);
53270
+ }
53271
+ stopStreamingEndTimer() {
53272
+ if (!this.streamingEndTimer)
53273
+ return;
53274
+ clearTimeout(this.streamingEndTimer);
53275
+ this.streamingEndTimer = null;
53276
+ }
53277
+ startSubmissionStartRetryTimer(attempt) {
53278
+ this.stopSubmissionStartRetryTimer();
53279
+ this.submissionStartRetryTimer = setTimeout$1(() => {
53280
+ this.submissionStartRetryTimer = null;
53281
+ this.onSubmissionStart(attempt);
53282
+ }, SUBMISSION_START_RETRY_MS);
53283
+ }
53284
+ stopSubmissionStartRetryTimer() {
53285
+ if (!this.submissionStartRetryTimer)
53286
+ return;
53287
+ clearTimeout(this.submissionStartRetryTimer);
53288
+ this.submissionStartRetryTimer = null;
53289
+ }
53290
+ suspend() {
53291
+ this.isActive = false;
53292
+ }
53293
+ resume() {
53294
+ this.isActive = true;
53295
+ }
53296
+ teardown() {
53297
+ this._.each(this.observers, (o) => o && o.disconnect && o.disconnect());
53298
+ this.observers = [];
53299
+ if (this.reactionClickHandler && this.root) {
53300
+ this.root.removeEventListener('click', this.reactionClickHandler, true);
53301
+ this.reactionClickHandler = null;
53302
+ }
53303
+ this.stopStreamingEndTimer();
53304
+ this.stopSubmissionStartRetryTimer();
53305
+ this.listeners = [];
53306
+ }
53307
+ }
53308
+
53309
+ /*
53310
+ * Conversation Analytics Plugin
53311
+ *
53312
+ * Independent plugin that tracks AI-agent conversations rendered in the DOM.
53313
+ *
53314
+ * The sibling built-in PromptAnalytics plugin owns the network + DOMPrompt AI-agents
53315
+ */
53316
+ function ConversationAnalytics() {
53317
+ let pendo;
53318
+ let api;
53319
+ let _;
53320
+ let log;
53321
+ let conversations = [];
53322
+ let agentsCache = new Map();
53323
+ const CONFIG_NAME = 'aiAgents';
53324
+ return {
53325
+ name: 'ConversationAnalytics',
53326
+ initialize,
53327
+ teardown,
53328
+ onConfigLoaded,
53329
+ observeConversation,
53330
+ sendConversationEvent,
53331
+ reEvaluatePageRules
53332
+ };
53333
+ function initialize(pendoGlobal, PluginAPI) {
53334
+ var _a;
53335
+ pendo = pendoGlobal;
53336
+ api = PluginAPI;
53337
+ _ = pendo._;
53338
+ log = api.log;
53339
+ const { ConfigReader } = api;
53340
+ ConfigReader.addOption(CONFIG_NAME, [
53341
+ ConfigReader.sources.PENDO_CONFIG_SRC,
53342
+ ConfigReader.sources.SNIPPET_SRC
53343
+ ]);
53344
+ onConfigLoaded(ConfigReader.get(CONFIG_NAME));
53345
+ // Listen for URL changes to re-evaluate page rules
53346
+ // Listening on guidesLoaded since normalizedUrl comes back from the guides playload
53347
+ (_a = api.Events.guidesLoaded) === null || _a === void 0 ? void 0 : _a.on(reEvaluatePageRules);
53348
+ }
53349
+ // Test if the current URL matches the page rule for an agent. The raw matcher lives in core
53350
+ // and is reached through PluginAPI (independent plugins cannot import it directly); it falls
53351
+ // back to the normalized URL internally when no URL is passed.
53352
+ function testPageRule(pageRule, agentId = 'unknown') {
53353
+ // Convert //*/agent pattern to work with full URLs
53354
+ let fixedPageRule = pageRule;
53355
+ if (Array.isArray(pageRule)) {
53356
+ const validRules = _.filter(pageRule, rule => rule && rule.trim() !== '');
53357
+ fixedPageRule = _.map(validRules, rule => rule.startsWith('//*/') ? rule.substring(1) : rule);
53358
+ }
53359
+ else if (typeof pageRule === 'string' && pageRule.startsWith('//*/')) {
53360
+ fixedPageRule = pageRule.substring(1);
53361
+ }
53362
+ return api.util.testPageRule(fixedPageRule, undefined, {
53363
+ logger: log,
53364
+ context: 'agent',
53365
+ id: agentId
53366
+ });
53367
+ }
53368
+ function reEvaluatePageRules() {
53369
+ _.each(conversations, conversation => {
53370
+ const agent = findAgentById(conversation.id);
53371
+ const shouldBeActive = agent && testPageRule(agent.pageRule, agent.id);
53372
+ if (shouldBeActive) {
53373
+ conversation.resume();
53374
+ }
53375
+ else {
53376
+ conversation.suspend();
53377
+ }
53378
+ });
53379
+ const currentlyActiveIds = _.map(_.filter(conversations, conversation => conversation.isActive), conversation => conversation.id);
53380
+ const allAgents = api.ConfigReader.get(CONFIG_NAME) || [];
53381
+ _.each(allAgents, aiAgent => {
53382
+ if (!isConversationAgent(aiAgent)) {
53383
+ return;
53384
+ }
53385
+ const isCurrentlyActive = _.contains(currentlyActiveIds, aiAgent.id);
53386
+ const shouldBeActive = testPageRule(aiAgent.pageRule, aiAgent.id);
53387
+ if (shouldBeActive && !isCurrentlyActive) {
53388
+ observeConversation(aiAgent);
53389
+ }
53390
+ });
53391
+ }
53392
+ function findAgentById(agentId) {
53393
+ return agentsCache.get(agentId);
53394
+ }
53395
+ // Only conversation agents belong to this plugin; the rest are owned
53396
+ // by the built-in PromptAnalytics plugin.
53397
+ function isConversationAgent(agent) {
53398
+ return DOMConversation.supportedPreset(agent.preset);
53399
+ }
53400
+ function onConfigLoaded(aiAgents = []) {
53401
+ if (!aiAgents || !_.isArray(aiAgents) || aiAgents.length === 0) {
53402
+ return;
53403
+ }
53404
+ agentsCache.clear();
53405
+ _.each(aiAgents, aiAgent => {
53406
+ if (!isConversationAgent(aiAgent)) {
53407
+ return;
53408
+ }
53409
+ agentsCache.set(aiAgent.id, aiAgent);
53410
+ if (testPageRule(aiAgent.pageRule, aiAgent.id)) {
53411
+ observeConversation(aiAgent);
53412
+ }
53413
+ });
53414
+ }
53415
+ function observeConversation(config) {
53416
+ const { id, cssSelectors } = config;
53417
+ // Check if this agent is already being tracked
53418
+ const existing = _.find(conversations, conversation => conversation.id === id);
53419
+ if (existing) {
53420
+ return;
53421
+ }
53422
+ const missing = DOMConversation.validateConfig(cssSelectors);
53423
+ if (missing.length) {
53424
+ log.error('aiAgent config missing required selectors', { agentId: id, missing });
53425
+ return;
53426
+ }
53427
+ const conversation = new DOMConversation(pendo, api, id, cssSelectors);
53428
+ conversation.onSubmit(sendConversationEvent);
53429
+ conversations.push(conversation);
53430
+ }
53431
+ function sendConversationEvent(eventType, eventPayload) {
53432
+ // Block event if the conversation is suspended
53433
+ const conversation = _.find(conversations, c => c.id === eventPayload.agentId);
53434
+ if (conversation && !conversation.isActive) {
53435
+ return;
53436
+ }
53437
+ eventPayload = _.extend({ agentType: 'conversation' }, eventPayload);
53438
+ api.analytics.collectEvent(eventType, eventPayload, undefined, 'agentic');
53439
+ }
53440
+ function teardown() {
53441
+ _.each(conversations, conversation => conversation.teardown());
53442
+ conversations = [];
53443
+ agentsCache.clear();
53444
+ }
53445
+ }
53446
+
53447
+ export { ConsoleCapture, ConversationAnalytics, Feedback, GuideMarkdown, NetworkCapture, PredictGuides, createReplayPlugin as Replay, TextCapture, VocPortal, loadAgent };