@pendo/agent 2.328.2 → 2.329.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.
@@ -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.329.0_';
4014
+ let PACKAGE_VERSION = '2.329.0';
4015
4015
  let LOADER = 'xhr';
4016
4016
  /* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
4017
4017
  /**
@@ -20097,6 +20097,7 @@ var GuideStateModule = (function () {
20097
20097
  storageKey: LAST_STEP_ADVANCED_COOKIE,
20098
20098
  guideRequestExpiration: 60 * 60 * 1000,
20099
20099
  scopedStorageKey: null,
20100
+ scopedThrottlingStorageKeys: {},
20100
20101
  receivedStateChangeAt: null,
20101
20102
  receivedLastGuideStepSeen: null,
20102
20103
  lastGuideStepSeen: null,
@@ -20119,6 +20120,11 @@ var GuideStateModule = (function () {
20119
20120
  */
20120
20121
  context.getters.storage().registry.addLocal(LAST_STEP_ADVANCED_COOKIE);
20121
20122
  context.commit('setScopedStorageKey', getPendoCookieKey(context.state.storageKey));
20123
+ var scopedThrottlingStorageKeys = {};
20124
+ _.each(THROTTLING_STATE, function (throttlingStorageKey) {
20125
+ scopedThrottlingStorageKeys[throttlingStorageKey] = getPendoCookieKey(throttlingStorageKey);
20126
+ });
20127
+ context.commit('setScopedThrottlingStorageKeys', scopedThrottlingStorageKeys);
20122
20128
  Events.identify.on(function (evt) {
20123
20129
  if (wasSessionCleared(evt)) {
20124
20130
  removeIdentificationCookies([LAST_STEP_ADVANCED_COOKIE]);
@@ -20157,11 +20163,21 @@ var GuideStateModule = (function () {
20157
20163
  });
20158
20164
  }
20159
20165
  }
20166
+ context.dispatch('loadThrottling');
20167
+ },
20168
+ loadThrottling(context) {
20169
+ const storage = context.getters.storage();
20160
20170
  _.each(THROTTLING_STATE, function (throttlingStorageKey) {
20161
20171
  const value = storage.read(throttlingStorageKey);
20162
20172
  context.dispatch('updateThrottlingState', { name: throttlingStorageKey, value });
20163
20173
  });
20164
20174
  },
20175
+ applyThrottling(context) {
20176
+ _.each(THROTTLING_STATE, function (throttlingStorageKey) {
20177
+ pendo$1[throttlingStorageKey] = applyTimerCache(pendo$1[throttlingStorageKey], context.state[throttlingStorageKey]);
20178
+ context.dispatch('updateThrottlingState', { name: throttlingStorageKey, value: pendo$1[throttlingStorageKey] });
20179
+ });
20180
+ },
20165
20181
  forceExpire(context) {
20166
20182
  _.each(context.state.steps, function (stepState, stepId) {
20167
20183
  context.commit('expireStepState', stepId);
@@ -20237,10 +20253,7 @@ var GuideStateModule = (function () {
20237
20253
  if (mostRecentLastGuideStepSeen) {
20238
20254
  context.dispatch('updateLastGuideStepSeen', _.extend({}, context.state.lastGuideStepSeen, mostRecentLastGuideStepSeen));
20239
20255
  }
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
- });
20256
+ context.dispatch('applyThrottling');
20244
20257
  },
20245
20258
  receiveLastGuideStepSeen(context, lastGuideStepSeen) {
20246
20259
  context.commit('setReceivedLastGuideStepSeen', lastGuideStepSeen);
@@ -20303,6 +20316,10 @@ var GuideStateModule = (function () {
20303
20316
  }
20304
20317
  }
20305
20318
  }
20319
+ else if (_.contains(_.values(context.state.scopedThrottlingStorageKeys), storageEvent.key)) {
20320
+ context.dispatch('loadThrottling');
20321
+ context.dispatch('applyThrottling');
20322
+ }
20306
20323
  },
20307
20324
  regainFocus(context) {
20308
20325
  var tabLostFocus = context.getters.tabLostFocus();
@@ -20330,6 +20347,9 @@ var GuideStateModule = (function () {
20330
20347
  setScopedStorageKey(state, scopedStorageKey) {
20331
20348
  state.scopedStorageKey = scopedStorageKey;
20332
20349
  },
20350
+ setScopedThrottlingStorageKeys(state, scopedThrottlingStorageKeys) {
20351
+ state.scopedThrottlingStorageKeys = scopedThrottlingStorageKeys;
20352
+ },
20333
20353
  setLastGuideStepSeen(state, lastGuideStepSeen) {
20334
20354
  state.lastGuideStepSeen = lastGuideStepSeen;
20335
20355
  },
@@ -23570,13 +23590,13 @@ var onGuideDismissed = function (evt, step) {
23570
23590
  var seenReason = firstStep && firstStep.seenReason;
23571
23591
  var language = currentGuide && currentGuide.language;
23572
23592
  var dismissCount = _.get(step, 'dismissCount', 0) + 1;
23573
- dismissedGuide(guideId, stepId, get_visitor_id(), seenReason, language, dismissCount);
23574
23593
  var now = getNow();
23575
- _updateGuideStepStatus(guideId, stepId, 'dismissed', now, dismissCount);
23576
- // maintain latestDismissedAutoAt client-side
23594
+ // maintain latestDismissedAutoAt client-side before guideDismissed writes lastStepAdvanced
23577
23595
  if (shouldAffectThrottling(currentGuide, seenReason)) {
23578
23596
  writeThrottlingStateCache(now, THROTTLING_STATE.DISMISSED);
23579
23597
  }
23598
+ dismissedGuide(guideId, stepId, get_visitor_id(), seenReason, language, dismissCount);
23599
+ _updateGuideStepStatus(guideId, stepId, 'dismissed', now, dismissCount);
23580
23600
  step.hide();
23581
23601
  if (!isGuideShown()) {
23582
23602
  stopGuides();
@@ -23632,12 +23652,12 @@ var onGuideSnoozed = function (evt, step, snoozeDuration) {
23632
23652
  log.info('snoozing guide for ' + snoozeDuration + ' ms');
23633
23653
  var snoozeEndTime = now + snoozeDuration;
23634
23654
  step.snoozeEndTime = snoozeEndTime;
23635
- snoozedGuide(guideId, stepId, visitorId, seenReason, language, snoozeDuration);
23636
- _updateGuideStepStatus(guideId, stepId, 'snoozed', now);
23637
- // maintain latestSnoozedAutoAt client-side
23655
+ // maintain latestSnoozedAutoAt client-side before guideSnoozed writes lastStepAdvanced
23638
23656
  if (shouldAffectThrottling(guide, seenReason)) {
23639
23657
  writeThrottlingStateCache(now, THROTTLING_STATE.SNOOZED);
23640
23658
  }
23659
+ snoozedGuide(guideId, stepId, visitorId, seenReason, language, snoozeDuration);
23660
+ _updateGuideStepStatus(guideId, stepId, 'snoozed', now);
23641
23661
  step.hide();
23642
23662
  if (!isGuideShown()) {
23643
23663
  stopGuides();
@@ -23786,15 +23806,15 @@ var onGuideAdvanced = function (evt, step, isIntermediateStep) {
23786
23806
  if (currentGuide && _.last(currentGuide.steps).id === stepId) {
23787
23807
  dismissCount = _.get(step, 'dismissCount', 0) + 1;
23788
23808
  }
23789
- log.info('advancing guide');
23790
- advancedGuide(guideId, stepId, get_visitor_id(), seenReason, language, isIntermediateStep, destinationStepId, dismissCount);
23791
- log.info('update guide status');
23792
23809
  var now = new Date().getTime();
23793
- _updateGuideStepStatus(guideId, stepId, 'advanced', now, dismissCount, destinationStepId);
23794
- // maintain finalAdvancedAutoAt client-side
23810
+ // maintain finalAdvancedAutoAt client-side before guideAdvanced writes lastStepAdvanced
23795
23811
  if (shouldAffectThrottling(currentGuide, seenReason)) {
23796
23812
  writeThrottlingStateCache(now, THROTTLING_STATE.ADVANCED);
23797
23813
  }
23814
+ log.info('advancing guide');
23815
+ advancedGuide(guideId, stepId, get_visitor_id(), seenReason, language, isIntermediateStep, destinationStepId, dismissCount);
23816
+ log.info('update guide status');
23817
+ _updateGuideStepStatus(guideId, stepId, 'advanced', now, dismissCount, destinationStepId);
23798
23818
  log.info('stop guide');
23799
23819
  stopGuides();
23800
23820
  log.info('start guides');
@@ -26035,7 +26055,7 @@ function getWhiteLabelSettings(config) {
26035
26055
  var whiteLabelSettings = ConfigReader.get('whiteLabelSettings');
26036
26056
  if (!whiteLabelSettings && config && config.ux === 'novus') {
26037
26057
  whiteLabelSettings = {
26038
- logoUrl: 'https://novus.pendo.io/novus-square-icon.svg',
26058
+ logoUrl: 'https://novus.pendo.io/branding/novus-vector-white.svg',
26039
26059
  logoStyle: {
26040
26060
  maxWidth: '40px',
26041
26061
  maxHeight: '40px'
@@ -28505,6 +28525,7 @@ const PluginAPI = {
28505
28525
  trim,
28506
28526
  base64EncodeString,
28507
28527
  parseUrl,
28528
+ testPageRule,
28508
28529
  addInlineStyles: _.partial(addInlineStyles, ConfigReader),
28509
28530
  getNow,
28510
28531
  escapeRegExp
@@ -40248,364 +40269,6 @@ class DOMPrompt {
40248
40269
  }
40249
40270
  }
40250
40271
 
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
40272
  /*
40610
40273
  * Prompt Analytics Plugin
40611
40274
  *
@@ -40690,6 +40353,9 @@ class PromptPlugin {
40690
40353
  });
40691
40354
  const currentlyActiveIds = this._.map(this._.filter(this.prompts, prompt => prompt.isActive), prompt => prompt.id);
40692
40355
  this._.each(allAgents, aiAgent => {
40356
+ if (this.isConversationAgent(aiAgent)) {
40357
+ return;
40358
+ }
40693
40359
  const isCurrentlyActive = this._.contains(currentlyActiveIds, aiAgent.id);
40694
40360
  const shouldBeActive = this.testPageRule(aiAgent.pageRule, currentUrl, aiAgent.id);
40695
40361
  if (shouldBeActive && !isCurrentlyActive) {
@@ -40708,6 +40374,10 @@ class PromptPlugin {
40708
40374
  return candidateValue;
40709
40375
  return candidateValue.replace(filters, 'redacted');
40710
40376
  }
40377
+ // conversation agents, owned by the ConversationAnalytics plugin.
40378
+ isConversationAgent(agent) {
40379
+ return agent.preset && agent.preset != 'chatgpt';
40380
+ }
40711
40381
  onConfigLoaded(aiAgents = []) {
40712
40382
  if (!aiAgents || !this._.isArray(aiAgents) || aiAgents.length === 0) {
40713
40383
  return;
@@ -40715,6 +40385,9 @@ class PromptPlugin {
40715
40385
  const networkAgents = [];
40716
40386
  const domAgents = [];
40717
40387
  for (const agent of aiAgents) {
40388
+ if (this.isConversationAgent(agent)) {
40389
+ continue;
40390
+ }
40718
40391
  if (agent.trackingMethod === 'network') {
40719
40392
  networkAgents.push(agent);
40720
40393
  }
@@ -40770,25 +40443,12 @@ class PromptPlugin {
40770
40443
  }
40771
40444
  }
40772
40445
  observePrompt(config) {
40773
- const { id, cssSelectors, privacyFilters, preset } = config;
40446
+ const { id, cssSelectors, privacyFilters } = config;
40774
40447
  // Check if agent already exists
40775
40448
  const existingPrompt = this._.find(this.prompts, prompt => prompt.id === id);
40776
40449
  if (existingPrompt) {
40777
40450
  return;
40778
40451
  }
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
40452
  let inputCssSelectors, submitCssSelectors;
40793
40453
  inputCssSelectors = this.parseCssSelector('input', cssSelectors);
40794
40454
  submitCssSelectors = this.parseCssSelector('submit', cssSelectors);
@@ -40815,15 +40475,6 @@ class PromptPlugin {
40815
40475
  }, promptEvent);
40816
40476
  this.api.analytics.collectEvent('prompt', event, undefined, 'agentic');
40817
40477
  }
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
40478
  teardown() {
40828
40479
  this._.each(this.prompts, (prompt) => prompt.teardown());
40829
40480
  if (this.agentsCache) {
@@ -41210,10 +40861,7 @@ class WebAnalytics {
41210
40861
  PluginAPI.attachEvent(PluginAPI.Events, 'eventCaptured', bind(this.addUtmToEvent, this)),
41211
40862
  PluginAPI.attachEvent(PluginAPI.Events, 'sessionChanged', bind(this.sessionChanged, this))
41212
40863
  ];
41213
- let params = this.loadParameters(PluginAPI.agentStorage);
41214
- if (!params || pendo._.isEmpty(params)) {
41215
- params = this.extractParameters(window.location.href, document.referrer);
41216
- }
40864
+ this.suffix = this.api.ConfigReader.get('identityStorageSuffix');
41217
40865
  /**
41218
40866
  * Stores UTM parameters that were present in the URL when the visitor loaded the application.
41219
40867
  *
@@ -41222,9 +40870,12 @@ class WebAnalytics {
41222
40870
  * @access public
41223
40871
  * @label WEB_ANALYTICS_STORAGE_KEY
41224
40872
  */
41225
- this.api.agentStorage.registry.addLocal(WEB_ANALYTICS_STORAGE_KEY);
40873
+ this.api.agentStorage.registry.addLocal(WEB_ANALYTICS_STORAGE_KEY, { cookieSuffix: this.suffix, isSecure: true });
40874
+ let params = this.loadParameters(PluginAPI.agentStorage);
40875
+ if (!params || pendo._.isEmpty(params)) {
40876
+ params = this.extractParameters(window.location.href, document.referrer);
40877
+ }
41226
40878
  this.storeParameters(params, PluginAPI.agentStorage);
41227
- this.suffix = this.api.ConfigReader.get('identityStorageSuffix');
41228
40879
  }
41229
40880
  extractParameters(url, referrer) {
41230
40881
  const locationUrl = new URL(url);
@@ -41279,12 +40930,12 @@ class WebAnalytics {
41279
40930
  }
41280
40931
  storeParameters(params, storage) {
41281
40932
  if (this.pendo._.size(params) > 0) {
41282
- storage.write(WEB_ANALYTICS_STORAGE_KEY, JSON.stringify(params), undefined, false, true, this.suffix);
40933
+ storage.write(WEB_ANALYTICS_STORAGE_KEY, JSON.stringify(params));
41283
40934
  }
41284
40935
  }
41285
40936
  loadParameters(storage) {
41286
- const storedParamsJson = storage.read(WEB_ANALYTICS_STORAGE_KEY, false, this.suffix) ||
41287
- storage.read(WEB_ANALYTICS_STORAGE_KEY);
40937
+ const storedParamsJson = storage.read(WEB_ANALYTICS_STORAGE_KEY) ||
40938
+ storage.read(WEB_ANALYTICS_STORAGE_KEY, false, '');
41288
40939
  if (storedParamsJson)
41289
40940
  return JSON.parse(storedParamsJson);
41290
40941
  return null;
@@ -41310,7 +40961,7 @@ class WebAnalytics {
41310
40961
  sessionChanged() {
41311
40962
  const agentStorage = this.api.agentStorage;
41312
40963
  agentStorage.clear(WEB_ANALYTICS_STORAGE_KEY);
41313
- agentStorage.clear(WEB_ANALYTICS_STORAGE_KEY, false, this.suffix);
40964
+ agentStorage.clear(WEB_ANALYTICS_STORAGE_KEY, false, '');
41314
40965
  const params = this.extractParameters(window.location.href, document.referrer);
41315
40966
  this.storeParameters(params, agentStorage);
41316
40967
  }
@@ -41805,6 +41456,21 @@ function branchingGoToStep(event, step, guide, pendo) {
41805
41456
  event.cancel = true;
41806
41457
  }
41807
41458
 
41459
+ var DANGEROUS_URL_SCHEME = /^\s*(javascript|data|vbscript):/i;
41460
+ function encodeMetadataForUrl(value, urlBeforePlaceholder) {
41461
+ if (urlBeforePlaceholder === void 0) { urlBeforePlaceholder = ''; }
41462
+ var str = value === undefined || value === null ? '' : String(value);
41463
+ var inUrlComponent = urlBeforePlaceholder.indexOf('?') !== -1 || urlBeforePlaceholder.indexOf('#') !== -1;
41464
+ if (inUrlComponent) {
41465
+ return window.encodeURIComponent(str);
41466
+ }
41467
+ var schemeAlreadySet = urlBeforePlaceholder.indexOf(':') !== -1;
41468
+ if (!schemeAlreadySet && DANGEROUS_URL_SCHEME.test(str)) {
41469
+ return '';
41470
+ }
41471
+ return window.encodeURI(str);
41472
+ }
41473
+
41808
41474
  const MetadataSubstitution = {
41809
41475
  name: 'MetadataSubstitution',
41810
41476
  script(step, guide, pendo) {
@@ -41948,8 +41614,10 @@ function substituteMetadataByTarget(data, target, mdValue, matched) {
41948
41614
  ? (data.getAttribute(target) || '')
41949
41615
  : data[target];
41950
41616
  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);
41617
+ const decodedUrl = decodeURI(current);
41618
+ const urlBeforePlaceholder = decodedUrl.slice(0, decodedUrl.indexOf(matched[0]));
41619
+ const safeValue = encodeMetadataForUrl(mdValue, urlBeforePlaceholder);
41620
+ data[target] = decodedUrl.replace(matched[0], safeValue);
41953
41621
  }
41954
41622
  else {
41955
41623
  data[target] = current.replace(matched[0], mdValue);
@@ -53384,4 +53052,500 @@ const PredictGuides = () => {
53384
53052
  };
53385
53053
  };
53386
53054
 
53387
- export { ConsoleCapture, Feedback, GuideMarkdown, NetworkCapture, PredictGuides, createReplayPlugin as Replay, TextCapture, VocPortal, loadAgent };
53055
+ const ASSISTANT_MESSAGE = 'assistantMessage';
53056
+ const CONVERSATION_ID_URL_PATTERN = 'conversationIdUrlPattern';
53057
+ const MESSAGE_ID_ATTR = 'messageIdAttr';
53058
+ const MODEL_USED_ATTR = 'modelUsedAttr';
53059
+ const REACTION_ACTIVE = 'reactionActive';
53060
+ const STREAMING_ACTIVE = 'streamingActive';
53061
+ const STREAMING_ACTIVE_ATTR = 'streamingActiveAttr';
53062
+ const THUMB_DOWN = 'thumbDown';
53063
+ const THUMB_UP = 'thumbUp';
53064
+ const TURN_CONTAINER = 'turnContainer';
53065
+ const TURN_ATTR = 'turnAttr';
53066
+ const USER_MESSAGE = 'userMessage';
53067
+ const CUSTOM_AGENT_ID_URL_PATTERN = 'customAgentIdUrlPattern';
53068
+ const CUSTOM_AGENT_NAME = 'customAgentName';
53069
+ const REQUIRED_SELECTORS = [
53070
+ ASSISTANT_MESSAGE,
53071
+ CONVERSATION_ID_URL_PATTERN,
53072
+ MESSAGE_ID_ATTR,
53073
+ STREAMING_ACTIVE,
53074
+ TURN_CONTAINER,
53075
+ TURN_ATTR,
53076
+ USER_MESSAGE
53077
+ ];
53078
+ const STREAMING_END_SETTLE_MS = 500;
53079
+ const SUBMISSION_START_RETRY_MS = 1000;
53080
+ const SUBMISSION_START_MAX_ATTEMPTS = 4;
53081
+ class DOMConversation {
53082
+ // Returns the list of required selector types that are missing or empty
53083
+ // in the given cssSelectors config. An empty array means the config is
53084
+ // valid for instantiation.
53085
+ static validateConfig(cssSelectors) {
53086
+ if (!Array.isArray(cssSelectors)) {
53087
+ return REQUIRED_SELECTORS.slice();
53088
+ }
53089
+ const present = new Set();
53090
+ for (const cfg of cssSelectors) {
53091
+ if (!cfg || !cfg.type || !cfg.cssSelector)
53092
+ continue;
53093
+ if (cfg.type === CONVERSATION_ID_URL_PATTERN) {
53094
+ try {
53095
+ RegExp(cfg.cssSelector);
53096
+ }
53097
+ catch (e) {
53098
+ continue;
53099
+ }
53100
+ }
53101
+ present.add(cfg.type);
53102
+ }
53103
+ return REQUIRED_SELECTORS.filter((r) => !present.has(r));
53104
+ }
53105
+ static supportedPreset(preset) {
53106
+ return preset === 'chatgpt-full';
53107
+ }
53108
+ constructor(pendo, PluginAPI, id, privacyFilters, cssSelectors) {
53109
+ this.selectors = {};
53110
+ this.listeners = [];
53111
+ this.isActive = true;
53112
+ this.observers = [];
53113
+ this.wasStreaming = false;
53114
+ this.streamingEndTimer = null;
53115
+ this.submissionStartRetryTimer = null;
53116
+ this.requestNode = null;
53117
+ this.conversationIdRegex = null;
53118
+ this.customAgentIdRegex = null;
53119
+ this.lastReturnedMessageId = null;
53120
+ this.dom = pendo.dom;
53121
+ this._ = pendo._;
53122
+ this.api = PluginAPI;
53123
+ this.Sizzle = pendo.Sizzle;
53124
+ this.id = id;
53125
+ this.setFilters(privacyFilters);
53126
+ if (!this._.isArray(cssSelectors)) {
53127
+ this.api.log.error(`cssSelectors must be an array, received: ${typeof cssSelectors}`);
53128
+ cssSelectors = [];
53129
+ }
53130
+ this._.each(cssSelectors, (cfg) => {
53131
+ if (cfg && cfg.type) {
53132
+ this.selectors[cfg.type] = cfg.cssSelector;
53133
+ }
53134
+ });
53135
+ this.conversationIdRegex = new RegExp(this.selectors[CONVERSATION_ID_URL_PATTERN]);
53136
+ const customAgentIdUrlPattern = this.selectors[CUSTOM_AGENT_ID_URL_PATTERN];
53137
+ if (customAgentIdUrlPattern) { // this is optional
53138
+ try {
53139
+ this.customAgentIdRegex = new RegExp(customAgentIdUrlPattern);
53140
+ }
53141
+ catch (error) {
53142
+ this.api.log.error(`bad customAgentIdUrlPattern ${customAgentIdUrlPattern}`, { error });
53143
+ }
53144
+ }
53145
+ this.root = document.body;
53146
+ this.setupReactionListener();
53147
+ this.setupStreamingObserver();
53148
+ }
53149
+ setupReactionListener() {
53150
+ if (!this.findSelector(THUMB_UP) && !this.findSelector(THUMB_DOWN))
53151
+ return;
53152
+ this.reactionClickHandler = this.handleReactionClick.bind(this);
53153
+ this.root.addEventListener('click', this.reactionClickHandler, true);
53154
+ }
53155
+ handleReactionClick(evt) {
53156
+ const target = evt.target;
53157
+ if (!target || target.nodeType !== 1)
53158
+ return;
53159
+ const upSelector = this.findSelector(THUMB_UP);
53160
+ const downSelector = this.findSelector(THUMB_DOWN);
53161
+ const thumb = this.dom(target).closest(`${upSelector}, ${downSelector}`);
53162
+ if (!thumb.length)
53163
+ return;
53164
+ const isUp = this.Sizzle.matchesSelector(thumb[0], upSelector);
53165
+ const activeSelector = this.findSelector(REACTION_ACTIVE);
53166
+ const wasPressed = activeSelector && this.Sizzle.matchesSelector(thumb[0], activeSelector);
53167
+ const direction = isUp ? 'positive' : 'negative';
53168
+ const content = wasPressed ? 'unreact' : direction;
53169
+ const assistantNode = thumb.closest(this.findSelector(TURN_CONTAINER))
53170
+ .find(this.findSelector(ASSISTANT_MESSAGE));
53171
+ this.emit('user_reaction', {
53172
+ agentId: this.id,
53173
+ messageId: this.getMessageId(assistantNode),
53174
+ content,
53175
+ privacyFilterApplied: false
53176
+ });
53177
+ }
53178
+ // observe the streaming indicator
53179
+ setupStreamingObserver() {
53180
+ this.wasStreaming = this.select(STREAMING_ACTIVE).length > 0;
53181
+ this.stopStreamingEndTimer();
53182
+ const MutationObserver = getZoneSafeMethod(this._, 'MutationObserver');
53183
+ const observer = new MutationObserver(this.handleMutation.bind(this));
53184
+ const attrName = this.findSelector(STREAMING_ACTIVE_ATTR);
53185
+ observer.observe(this.root, {
53186
+ childList: true,
53187
+ subtree: true,
53188
+ attributes: true,
53189
+ attributeFilter: attrName ? [attrName] : undefined
53190
+ });
53191
+ this.observers.push(observer);
53192
+ }
53193
+ // look for changes in the state of the streaming indicator.
53194
+ // streaming started -> a prompt has been submitted
53195
+ // streaming ended -> got a response
53196
+ // we don't send the prompt immediately when it's submitted because it might not have a
53197
+ // conversation id yet.
53198
+ // we don't send the agent_response immediately when the streaming indicator is cleared
53199
+ // because it clears slightly before the final text chunk is committed to the DOM so the
53200
+ // end-handling is deferred by a short settle window
53201
+ handleMutation() {
53202
+ const isStreaming = this.select(STREAMING_ACTIVE).length > 0;
53203
+ if (isStreaming === this.wasStreaming)
53204
+ return;
53205
+ if (isStreaming) { // started streaming
53206
+ if (!this.streamingEndTimer) {
53207
+ this.onSubmissionStart();
53208
+ }
53209
+ else {
53210
+ // stream restarted during the settle window (multi-phase response).
53211
+ // treat as a continuation of the prior request, not a new submission.
53212
+ // we'll handle it next time streaming stops
53213
+ this.stopStreamingEndTimer();
53214
+ }
53215
+ }
53216
+ else { // finished streaming
53217
+ this.startStreamingEndTimer();
53218
+ }
53219
+ this.wasStreaming = isStreaming;
53220
+ }
53221
+ onSubmissionStart(attempt = 1) {
53222
+ this.requestNode = this.findLastRequestNode();
53223
+ if (this.requestNode) {
53224
+ this.handleUserMessage(this.requestNode);
53225
+ return;
53226
+ }
53227
+ // URL hasn't flipped yet to include the conversation id on a fresh chat
53228
+ // retry, if that still fails, onSubmissionEnd handles it
53229
+ if (attempt < SUBMISSION_START_MAX_ATTEMPTS) {
53230
+ this.startSubmissionStartRetryTimer(attempt + 1);
53231
+ }
53232
+ }
53233
+ onSubmissionEnd() {
53234
+ this.stopSubmissionStartRetryTimer();
53235
+ let requestNode = this.requestNode;
53236
+ this.requestNode = null;
53237
+ if (!requestNode) {
53238
+ // onSubmissionStart didn't send, send it now
53239
+ requestNode = this.findLastRequestNode();
53240
+ if (!requestNode)
53241
+ return;
53242
+ this.handleUserMessage(requestNode);
53243
+ }
53244
+ const requestTurn = this.findTurnNumber(requestNode);
53245
+ if (requestTurn === null)
53246
+ return;
53247
+ const assistantNodes = this.select(ASSISTANT_MESSAGE);
53248
+ const toEmit = [];
53249
+ for (let i = assistantNodes.length - 1; i >= 0; i--) {
53250
+ const n = this.findTurnNumber(this.dom(assistantNodes[i]));
53251
+ if (n === null)
53252
+ continue;
53253
+ if (n <= requestTurn)
53254
+ break;
53255
+ toEmit.push(assistantNodes[i]);
53256
+ }
53257
+ // emit in chronological order (reverse of collection order)
53258
+ for (let i = toEmit.length - 1; i >= 0; i--) {
53259
+ this.handleAssistantMessage(this.dom(toEmit[i]));
53260
+ }
53261
+ }
53262
+ extractContent(node) {
53263
+ const rawContent = node.text().trim();
53264
+ const content = this.applyPrivacyFilter(rawContent);
53265
+ return { content, privacyFilterApplied: content !== rawContent };
53266
+ }
53267
+ getMessageId(node) {
53268
+ return node.attr(this.findSelector(MESSAGE_ID_ATTR));
53269
+ }
53270
+ handleUserMessage(node) {
53271
+ this.emit('prompt', Object.assign({ agentId: this.id, messageId: this.getMessageId(node) }, this.extractContent(node)));
53272
+ }
53273
+ handleAssistantMessage(node) {
53274
+ const modelUsedAttr = this.findSelector(MODEL_USED_ATTR);
53275
+ const modelUsed = modelUsedAttr && node.attr(modelUsedAttr);
53276
+ this.emit('agent_response', Object.assign({ agentId: this.id, messageId: this.getMessageId(node), agentModelsUsed: modelUsed ? [modelUsed] : [] }, this.extractContent(node)));
53277
+ }
53278
+ findLastRequestNode() {
53279
+ if (!this.getConversationId())
53280
+ return null;
53281
+ const node = this._.last(this.select(USER_MESSAGE));
53282
+ if (!node)
53283
+ return null;
53284
+ const $node = this.dom(node);
53285
+ const messageId = this.getMessageId($node);
53286
+ if (!messageId)
53287
+ return null;
53288
+ if (messageId === this.lastReturnedMessageId)
53289
+ return null;
53290
+ this.lastReturnedMessageId = messageId;
53291
+ return $node;
53292
+ }
53293
+ findTurnNumber(node) {
53294
+ const turnSelector = this.findSelector(TURN_CONTAINER);
53295
+ const turnAttr = this.findSelector(TURN_ATTR);
53296
+ const turn = node.closest(turnSelector).attr(turnAttr);
53297
+ if (!turn)
53298
+ return null;
53299
+ const seqMatch = String(turn).match(/\d+/);
53300
+ return seqMatch ? parseInt(seqMatch[0], 10) : null;
53301
+ }
53302
+ findSelector(type) {
53303
+ return this.selectors[type];
53304
+ }
53305
+ select(type) {
53306
+ return this.dom(this.findSelector(type), this.root);
53307
+ }
53308
+ getConversationId() {
53309
+ const m = location.href.match(this.conversationIdRegex);
53310
+ if (!m)
53311
+ return null;
53312
+ return m[1] !== undefined ? m[1] : m[0];
53313
+ }
53314
+ getCustomAgentId() {
53315
+ if (!this.customAgentIdRegex)
53316
+ return undefined;
53317
+ const m = location.href.match(this.customAgentIdRegex);
53318
+ if (!m)
53319
+ return undefined;
53320
+ return m[1] !== undefined ? m[1] : m[0];
53321
+ }
53322
+ getCustomAgentName() {
53323
+ if (!this.findSelector(CUSTOM_AGENT_NAME))
53324
+ return '';
53325
+ const node = this.select(CUSTOM_AGENT_NAME);
53326
+ if (!node.length)
53327
+ return '';
53328
+ return node.text().trim();
53329
+ }
53330
+ addCustomAgentInfo(eventPayload) {
53331
+ const customAgentId = this.getCustomAgentId();
53332
+ if (!customAgentId)
53333
+ return;
53334
+ eventPayload.customAgentId = customAgentId;
53335
+ const customAgentName = this.getCustomAgentName();
53336
+ if (customAgentName) {
53337
+ eventPayload.customAgentName = customAgentName;
53338
+ }
53339
+ }
53340
+ setFilters(candidateFilter) {
53341
+ if (!candidateFilter) {
53342
+ this.privacyFilters = null;
53343
+ return null;
53344
+ }
53345
+ try {
53346
+ this.privacyFilters = new RegExp(candidateFilter, 'gmi');
53347
+ }
53348
+ catch (e) {
53349
+ this.privacyFilters = null;
53350
+ this.api.log.error(e);
53351
+ }
53352
+ return this.privacyFilters;
53353
+ }
53354
+ applyPrivacyFilter(candidateValue, filters = null) {
53355
+ const filtersToUse = filters || this.privacyFilters;
53356
+ if (!filtersToUse || !this._.isRegExp(filtersToUse))
53357
+ return candidateValue;
53358
+ return candidateValue.replace(filtersToUse, 'redacted');
53359
+ }
53360
+ onSubmit(callback) {
53361
+ this.listeners.push(callback);
53362
+ }
53363
+ emit(eventType, eventPayload) {
53364
+ eventPayload.conversationId = this.getConversationId();
53365
+ this.addCustomAgentInfo(eventPayload);
53366
+ this._.each(this.listeners, (cb) => cb(eventType, eventPayload));
53367
+ }
53368
+ startStreamingEndTimer() {
53369
+ this.stopStreamingEndTimer();
53370
+ this.streamingEndTimer = setTimeout$1(() => {
53371
+ this.streamingEndTimer = null;
53372
+ this.onSubmissionEnd();
53373
+ }, STREAMING_END_SETTLE_MS);
53374
+ }
53375
+ stopStreamingEndTimer() {
53376
+ if (!this.streamingEndTimer)
53377
+ return;
53378
+ clearTimeout(this.streamingEndTimer);
53379
+ this.streamingEndTimer = null;
53380
+ }
53381
+ startSubmissionStartRetryTimer(attempt) {
53382
+ this.stopSubmissionStartRetryTimer();
53383
+ this.submissionStartRetryTimer = setTimeout$1(() => {
53384
+ this.submissionStartRetryTimer = null;
53385
+ this.onSubmissionStart(attempt);
53386
+ }, SUBMISSION_START_RETRY_MS);
53387
+ }
53388
+ stopSubmissionStartRetryTimer() {
53389
+ if (!this.submissionStartRetryTimer)
53390
+ return;
53391
+ clearTimeout(this.submissionStartRetryTimer);
53392
+ this.submissionStartRetryTimer = null;
53393
+ }
53394
+ suspend() {
53395
+ this.isActive = false;
53396
+ }
53397
+ resume() {
53398
+ this.isActive = true;
53399
+ }
53400
+ teardown() {
53401
+ this._.each(this.observers, (o) => o && o.disconnect && o.disconnect());
53402
+ this.observers = [];
53403
+ if (this.reactionClickHandler && this.root) {
53404
+ this.root.removeEventListener('click', this.reactionClickHandler, true);
53405
+ this.reactionClickHandler = null;
53406
+ }
53407
+ this.stopStreamingEndTimer();
53408
+ this.stopSubmissionStartRetryTimer();
53409
+ this.listeners = [];
53410
+ }
53411
+ }
53412
+
53413
+ /*
53414
+ * Conversation Analytics Plugin
53415
+ *
53416
+ * Independent plugin that tracks AI-agent conversations rendered in the DOM.
53417
+ *
53418
+ * The sibling built-in PromptAnalytics plugin owns the network + DOMPrompt AI-agents
53419
+ */
53420
+ function ConversationAnalytics() {
53421
+ let pendo;
53422
+ let api;
53423
+ let _;
53424
+ let log;
53425
+ let conversations = [];
53426
+ let agentsCache = new Map();
53427
+ const CONFIG_NAME = 'aiAgents';
53428
+ return {
53429
+ name: 'ConversationAnalytics',
53430
+ initialize,
53431
+ teardown,
53432
+ onConfigLoaded,
53433
+ observeConversation,
53434
+ sendConversationEvent,
53435
+ reEvaluatePageRules
53436
+ };
53437
+ function initialize(pendoGlobal, PluginAPI) {
53438
+ var _a;
53439
+ pendo = pendoGlobal;
53440
+ api = PluginAPI;
53441
+ _ = pendo._;
53442
+ log = api.log;
53443
+ const { ConfigReader } = api;
53444
+ ConfigReader.addOption(CONFIG_NAME, [
53445
+ ConfigReader.sources.PENDO_CONFIG_SRC,
53446
+ ConfigReader.sources.SNIPPET_SRC
53447
+ ]);
53448
+ onConfigLoaded(ConfigReader.get(CONFIG_NAME));
53449
+ // Listen for URL changes to re-evaluate page rules
53450
+ // Listening on guidesLoaded since normalizedUrl comes back from the guides playload
53451
+ (_a = api.Events.guidesLoaded) === null || _a === void 0 ? void 0 : _a.on(reEvaluatePageRules);
53452
+ }
53453
+ // Test if the current URL matches the page rule for an agent. The raw matcher lives in core
53454
+ // and is reached through PluginAPI (independent plugins cannot import it directly); it falls
53455
+ // back to the normalized URL internally when no URL is passed.
53456
+ function testPageRule(pageRule, agentId = 'unknown') {
53457
+ // Convert //*/agent pattern to work with full URLs
53458
+ let fixedPageRule = pageRule;
53459
+ if (Array.isArray(pageRule)) {
53460
+ const validRules = _.filter(pageRule, rule => rule && rule.trim() !== '');
53461
+ fixedPageRule = _.map(validRules, rule => rule.startsWith('//*/') ? rule.substring(1) : rule);
53462
+ }
53463
+ else if (typeof pageRule === 'string' && pageRule.startsWith('//*/')) {
53464
+ fixedPageRule = pageRule.substring(1);
53465
+ }
53466
+ return api.util.testPageRule(fixedPageRule, undefined, {
53467
+ logger: log,
53468
+ context: 'agent',
53469
+ id: agentId
53470
+ });
53471
+ }
53472
+ function reEvaluatePageRules() {
53473
+ _.each(conversations, conversation => {
53474
+ const agent = findAgentById(conversation.id);
53475
+ const shouldBeActive = agent && testPageRule(agent.pageRule, agent.id);
53476
+ if (shouldBeActive) {
53477
+ conversation.resume();
53478
+ }
53479
+ else {
53480
+ conversation.suspend();
53481
+ }
53482
+ });
53483
+ const currentlyActiveIds = _.map(_.filter(conversations, conversation => conversation.isActive), conversation => conversation.id);
53484
+ const allAgents = api.ConfigReader.get(CONFIG_NAME) || [];
53485
+ _.each(allAgents, aiAgent => {
53486
+ if (!isConversationAgent(aiAgent)) {
53487
+ return;
53488
+ }
53489
+ const isCurrentlyActive = _.contains(currentlyActiveIds, aiAgent.id);
53490
+ const shouldBeActive = testPageRule(aiAgent.pageRule, aiAgent.id);
53491
+ if (shouldBeActive && !isCurrentlyActive) {
53492
+ observeConversation(aiAgent);
53493
+ }
53494
+ });
53495
+ }
53496
+ function findAgentById(agentId) {
53497
+ return agentsCache.get(agentId);
53498
+ }
53499
+ // Only conversation agents belong to this plugin; the rest are owned
53500
+ // by the built-in PromptAnalytics plugin.
53501
+ function isConversationAgent(agent) {
53502
+ return DOMConversation.supportedPreset(agent.preset);
53503
+ }
53504
+ function onConfigLoaded(aiAgents = []) {
53505
+ if (!aiAgents || !_.isArray(aiAgents) || aiAgents.length === 0) {
53506
+ return;
53507
+ }
53508
+ agentsCache.clear();
53509
+ _.each(aiAgents, aiAgent => {
53510
+ if (!isConversationAgent(aiAgent)) {
53511
+ return;
53512
+ }
53513
+ agentsCache.set(aiAgent.id, aiAgent);
53514
+ if (testPageRule(aiAgent.pageRule, aiAgent.id)) {
53515
+ observeConversation(aiAgent);
53516
+ }
53517
+ });
53518
+ }
53519
+ function observeConversation(config) {
53520
+ const { id, cssSelectors, privacyFilters } = config;
53521
+ // Check if this agent is already being tracked
53522
+ const existing = _.find(conversations, conversation => conversation.id === id);
53523
+ if (existing) {
53524
+ return;
53525
+ }
53526
+ const missing = DOMConversation.validateConfig(cssSelectors);
53527
+ if (missing.length) {
53528
+ log.error('aiAgent config missing required selectors', { agentId: id, missing });
53529
+ return;
53530
+ }
53531
+ const conversation = new DOMConversation(pendo, api, id, privacyFilters, cssSelectors);
53532
+ conversation.onSubmit(sendConversationEvent);
53533
+ conversations.push(conversation);
53534
+ }
53535
+ function sendConversationEvent(eventType, eventPayload) {
53536
+ // Block event if the conversation is suspended
53537
+ const conversation = _.find(conversations, c => c.id === eventPayload.agentId);
53538
+ if (conversation && !conversation.isActive) {
53539
+ return;
53540
+ }
53541
+ eventPayload = _.extend({ agentType: 'conversation' }, eventPayload);
53542
+ api.analytics.collectEvent(eventType, eventPayload, undefined, 'agentic');
53543
+ }
53544
+ function teardown() {
53545
+ _.each(conversations, conversation => conversation.teardown());
53546
+ conversations = [];
53547
+ agentsCache.clear();
53548
+ }
53549
+ }
53550
+
53551
+ export { ConsoleCapture, ConversationAnalytics, Feedback, GuideMarkdown, NetworkCapture, PredictGuides, createReplayPlugin as Replay, TextCapture, VocPortal, loadAgent };