@pendo/agent 2.345.0 → 2.347.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.
@@ -668,7 +668,7 @@ function loadStagingAgent(config, pendo, debuggingEnabled) {
668
668
  var altAgentUrl = getAlternateAgentUrl(config, true, debuggingEnabled);
669
669
  if (altAgentUrl) {
670
670
  setupMethodCapture(pendo);
671
- includeScript(altAgentUrl, pendo);
671
+ includeScript(altAgentUrl, pendo, config.pendoGlobalKey);
672
672
  return true;
673
673
  }
674
674
  }
@@ -679,7 +679,7 @@ function loadUnminifiedAgent(config, pendo, debuggingEnabled) {
679
679
  var altAgentUrl = getAlternateAgentUrl(config, false, debuggingEnabled);
680
680
  if (altAgentUrl) {
681
681
  setupMethodCapture(pendo);
682
- includeScript(altAgentUrl, pendo);
682
+ includeScript(altAgentUrl, pendo, config.pendoGlobalKey);
683
683
  return true;
684
684
  }
685
685
  }
@@ -688,11 +688,16 @@ function loadUnminifiedAgent(config, pendo, debuggingEnabled) {
688
688
  function loadAlternateAgent(config, pendo, debuggingEnabled) {
689
689
  return loadStagingAgent(config, pendo, debuggingEnabled) || loadUnminifiedAgent(config, pendo, debuggingEnabled);
690
690
  }
691
- function includeScript(scriptUrl, pendo) {
691
+ function includeScript(scriptUrl, pendo, globalKey) {
692
692
  var scriptTagName = 'script';
693
693
  var stagingScriptTag = document.createElement(scriptTagName);
694
694
  stagingScriptTag.async = true;
695
695
  stagingScriptTag.src = getPolicy(pendo).createScriptURL(scriptUrl);
696
+ // The alternate agent reads its global key from its own script tag, and its
697
+ // bundled PendoConfig doesn't carry the key the snippet set.
698
+ if (globalKey) {
699
+ stagingScriptTag.setAttribute('data-pendo-global-key', globalKey);
700
+ }
696
701
  var otherScriptTag = document.getElementsByTagName(scriptTagName)[0];
697
702
  if (otherScriptTag && otherScriptTag.parentNode) {
698
703
  otherScriptTag.parentNode.insertBefore(stagingScriptTag, otherScriptTag);
@@ -3266,9 +3271,9 @@ var ConfigReader = (function () {
3266
3271
  */
3267
3272
  addOption('selfHostedWebSDKUrl', [SNIPPET_SRC, PENDO_CONFIG_SRC], undefined, undefined, ['selfHostedAgentUrl']);
3268
3273
  /**
3269
- * If this option is set to true, event data will only be transmitted using requests of type POST instead
3270
- * of first trying to use GET requests. This can result in fewer requests being sent each time that event data
3271
- * is transmitted.
3274
+ * If this option is set to true, event data, guide requests, and segment flag requests will only be
3275
+ * transmitted using requests of type POST instead of first trying to use GET requests. For event data, this
3276
+ * can result in fewer requests being sent each time that event data is transmitted.
3272
3277
  *
3273
3278
  * @access public
3274
3279
  * @category Config/Core
@@ -4009,6 +4014,9 @@ function shouldWrapHashChange() {
4009
4014
  function isMobileUserAgent() {
4010
4015
  return (/Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i).test(getUA());
4011
4016
  }
4017
+ function supportsPopover() {
4018
+ return !!window.HTMLElement && isNativeCode(window.HTMLElement.prototype.showPopover);
4019
+ }
4012
4020
  function isSafari(userAgent = getUA()) {
4013
4021
  return userAgent.indexOf('Safari') >= 0 && !isChromium(userAgent);
4014
4022
  }
@@ -4021,6 +4029,7 @@ function isChromium(userAgent = getUA()) {
4021
4029
  const sniffer = {
4022
4030
  supportsHistoryApi: shouldWrapNativeHistory,
4023
4031
  supportsHashChange: shouldWrapHashChange,
4032
+ supportsPopover,
4024
4033
  hasEvent(event) {
4025
4034
  // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
4026
4035
  // it. In particular the event is not fired when backspace or delete key are pressed or
@@ -4129,8 +4138,8 @@ let SERVER = '';
4129
4138
  let ASSET_HOST = '';
4130
4139
  let ASSET_PATH = '';
4131
4140
  let DESIGNER_SERVER = '';
4132
- let VERSION = '2.345.0_';
4133
- let PACKAGE_VERSION = '2.345.0';
4141
+ let VERSION = '2.347.0_';
4142
+ let PACKAGE_VERSION = '2.347.0';
4134
4143
  let LOADER = 'xhr';
4135
4144
  /* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
4136
4145
  /**
@@ -13123,7 +13132,7 @@ function isDebuggingEnabled(asBoolean = true) {
13123
13132
  }
13124
13133
  }
13125
13134
 
13126
- const PERFORMANCE_SEND_INTERVAL = 1000 * 60 * 10; // 10 minutes
13135
+ const PERFORMANCE_COLLECT_INTERVAL = 1000 * 60 * 10; // 10 minutes
13127
13136
  class PerformanceMonitor {
13128
13137
  constructor() {
13129
13138
  this._isPerformanceApiAvailable = this._checkPerformanceApi();
@@ -13131,6 +13140,8 @@ class PerformanceMonitor {
13131
13140
  this._measuringPerformance = false;
13132
13141
  this._debugging = false;
13133
13142
  this._setDebuggingBound = _.bind(this._setDebugging, this);
13143
+ this._sendBound = _.bind(this.send, this);
13144
+ this._totals = {};
13134
13145
  this.marks = new Set();
13135
13146
  this.measures = new Set();
13136
13147
  }
@@ -13141,12 +13152,13 @@ class PerformanceMonitor {
13141
13152
  this._markPerformance(INITIALIZE);
13142
13153
  this._setDebugging();
13143
13154
  Events.debuggerLaunched.on(this._setDebuggingBound);
13155
+ Events.appUnloaded.on(this._sendBound);
13144
13156
  const shouldSend = Math.random() * 100 < ConfigReader.get('performanceMetricsSampleRate');
13145
13157
  this._setSending(shouldSend);
13146
13158
  // interval always runs to periodically free memory used by performance api
13147
13159
  this.interval = setInterval(() => {
13148
- this.send(); // includes call to _clearMarksAndMeasures()
13149
- }, PERFORMANCE_SEND_INTERVAL);
13160
+ this._collect();
13161
+ }, PERFORMANCE_COLLECT_INTERVAL);
13150
13162
  return () => {
13151
13163
  this.teardown();
13152
13164
  };
@@ -13157,6 +13169,7 @@ class PerformanceMonitor {
13157
13169
  this._measuringPerformance = false;
13158
13170
  this._setDebugging();
13159
13171
  Events.debuggerLaunched.off(this._setDebuggingBound);
13172
+ Events.appUnloaded.off(this._sendBound);
13160
13173
  this._clearMarksAndMeasures();
13161
13174
  clearInterval(this.interval);
13162
13175
  }
@@ -13176,25 +13189,41 @@ class PerformanceMonitor {
13176
13189
  this._markPerformance(name, { detail });
13177
13190
  }
13178
13191
  send() {
13179
- const payload = _.reduce(METRICS, (acc, metric) => {
13180
- const { name, type, instrument } = metric;
13181
- if (!instrument)
13182
- return acc;
13183
- const entries = performance.getEntriesByName(name);
13184
- const value = instrument(entries);
13185
- if (value > 0) {
13186
- acc.push({
13187
- name,
13188
- type,
13189
- value,
13190
- timestamp: Date.now()
13191
- });
13192
+ // runs in the appUnloaded handler ahead of the agent's event flush, so it must not throw
13193
+ try {
13194
+ this._collect();
13195
+ const payload = _.map(this._totals, (value, name) => ({
13196
+ name,
13197
+ type: METRICS[name].type,
13198
+ value,
13199
+ timestamp: Date.now()
13200
+ }));
13201
+ this._totals = {};
13202
+ if (_.size(payload) && this._sending) {
13203
+ writeMetricsPOST(payload);
13192
13204
  }
13193
- return acc;
13194
- }, []);
13195
- this._clearMarksAndMeasures();
13196
- if (_.size(payload) && this._sending) {
13197
- writeMetricsPOST(payload);
13205
+ }
13206
+ catch (e) {
13207
+ log.debug(`Failed to send performance metrics: ${e}`);
13208
+ }
13209
+ }
13210
+ _collect() {
13211
+ if (!this._isPerformanceApiAvailable)
13212
+ return;
13213
+ try {
13214
+ if (!this._sending)
13215
+ return;
13216
+ _.each(METRICS, ({ name, instrument }) => {
13217
+ if (!instrument)
13218
+ return;
13219
+ const value = instrument(performance.getEntriesByName(name));
13220
+ if (value > 0) {
13221
+ this._totals[name] = (this._totals[name] || 0) + value;
13222
+ }
13223
+ });
13224
+ }
13225
+ finally {
13226
+ this._clearMarksAndMeasures();
13198
13227
  }
13199
13228
  }
13200
13229
  _checkPerformanceApi() {
@@ -13442,6 +13471,9 @@ function filterSiloCapacity(options) {
13442
13471
  };
13443
13472
  }
13444
13473
  var URL_MAX_LENGTH = 2000;
13474
+ function shouldLoadWithPost(url, jwtOptions) {
13475
+ return url.length > URL_MAX_LENGTH || !_.isEmpty(jwtOptions) || !!ConfigReader.get('sendEventsWithPostOnly');
13476
+ }
13445
13477
  function shortenUrlFields(options) {
13446
13478
  options = _.defaults(options || {}, {
13447
13479
  fields: [],
@@ -23855,6 +23887,78 @@ var backdropVisibilityStep = {
23855
23887
  script: backdropVisibilityRuntimeScript
23856
23888
  };
23857
23889
 
23890
+ // ":modal" also matches a fullscreen element, which cannot take children
23891
+ const MODAL_DIALOG = 'dialog:modal';
23892
+ function queryModalDialogs() {
23893
+ try {
23894
+ return document.querySelectorAll(MODAL_DIALOG);
23895
+ }
23896
+ catch (e) {
23897
+ return [];
23898
+ }
23899
+ }
23900
+ function closestModalDialog(element) {
23901
+ try {
23902
+ return element && _.isFunction(element.closest) ? element.closest(MODAL_DIALOG) : null;
23903
+ }
23904
+ catch (e) {
23905
+ return null;
23906
+ }
23907
+ }
23908
+ function queryConfiguredSelector(step) {
23909
+ const selector = _.get(step, 'attributes.popover.selector');
23910
+ if (!selector)
23911
+ return null;
23912
+ try {
23913
+ return dom(selector)[0] || null;
23914
+ }
23915
+ catch (e) {
23916
+ log.info('Invalid popover selector', { contexts: ['error', 'guide', 'render'] });
23917
+ return null;
23918
+ }
23919
+ }
23920
+ function findHost(step) {
23921
+ const host = queryConfiguredSelector(step) ||
23922
+ closestModalDialog(step.targetElement) ||
23923
+ _.last(queryModalDialogs());
23924
+ // a host inside a shadow root would change the guide's root node
23925
+ return host && host.getRootNode() === document ? host : null;
23926
+ }
23927
+ function promoteToPopover(element) {
23928
+ element.setAttribute('popover', 'manual');
23929
+ try {
23930
+ element.showPopover();
23931
+ }
23932
+ catch (e) {
23933
+ element.removeAttribute('popover');
23934
+ log.info('Failed to render guide as popover', { contexts: ['error', 'guide', 'render'] });
23935
+ }
23936
+ }
23937
+ // Registered ahead of backdrop-visibility-step and autofocus-step, which mark the siblings of the
23938
+ // guide's ancestors: the guide must already be inside the dialog or the dialog ends up hidden.
23939
+ function popoverRuntimeScript(step, guide, pendo) {
23940
+ if (pendo.designer || !step.guideElement)
23941
+ return;
23942
+ if (!_.get(step, 'attributes.popover.enabled') || !sniffer.supportsPopover())
23943
+ return;
23944
+ if (_.get(guide, 'attributes.resourceCenter') || _.get(guide, 'attributes.embedConfig'))
23945
+ return;
23946
+ const guideContainer = step.guideElement.find(`#${step.containerId}`);
23947
+ if (!guideContainer.length)
23948
+ return;
23949
+ // positioned in document coordinates, which a fixed root in the top layer breaks
23950
+ if (guideContainer.attr('data-vertical-alignment') === 'Relative to Element')
23951
+ return;
23952
+ const host = findHost(step);
23953
+ if (!host)
23954
+ return;
23955
+ host.appendChild(step.guideElement[0]);
23956
+ promoteToPopover(step.guideElement[0]);
23957
+ }
23958
+ var popoverStep = {
23959
+ script: popoverRuntimeScript
23960
+ };
23961
+
23858
23962
  function isTooltip({ _ }, step) {
23859
23963
  const tooltip = 'tooltip';
23860
23964
  return _.get(step, 'attributes.calculatedType') === tooltip ||
@@ -24011,6 +24115,7 @@ function registerPostrenderScripts(runtime) {
24011
24115
  _.each([
24012
24116
  advanceOnElement,
24013
24117
  announcementMarkSeen,
24118
+ popoverStep,
24014
24119
  autofocusStep,
24015
24120
  backdropVisibilityStep,
24016
24121
  repositionOnScroll,
@@ -25113,7 +25218,7 @@ var loadGuideJs = function (apiKey, params) {
25113
25218
  var url = buildBaseDataUrl(guideLoaderImpl.endpoint(), apiKey, queryString);
25114
25219
  var jwtOptions = JWT.get();
25115
25220
  let loadingPromise;
25116
- if (url.length > URL_MAX_LENGTH || !_.isEmpty(jwtOptions)) {
25221
+ if (shouldLoadWithPost(url, jwtOptions)) {
25117
25222
  delete queryString.jzb;
25118
25223
  url = buildBaseDataUrl('guide.json', apiKey, queryString);
25119
25224
  var payload = _.extend({ events: jzb }, jwtOptions);
@@ -25966,8 +26071,14 @@ var MESSAGE_ENDED = 'pendo-live-preview::ended';
25966
26071
  var MESSAGE_END_ACK = 'pendo-live-preview::end-ack';
25967
26072
  var RELAY_END_MESSAGE_TIMEOUT = 2000;
25968
26073
  var activeRelay = null;
26074
+ // event.source alone is not enough: an ancestor page can navigate the relay frame to its own
26075
+ // document and keep the same window object, so the sender's origin has to be the relay's too.
26076
+ // An unknown relay origin ('*') cannot be matched, so it never authenticates a message.
25969
26077
  function isRelayMessage(event, relay) {
25970
- return event.source === relay.frame.contentWindow && !!event.data;
26078
+ return relay.origin !== '*' &&
26079
+ event.origin === relay.origin &&
26080
+ event.source === relay.frame.contentWindow &&
26081
+ !!event.data;
25971
26082
  }
25972
26083
  function sendConfig(relay) {
25973
26084
  if (!relay.frame.contentWindow)
@@ -28502,7 +28613,8 @@ class NetworkRequestIntercept {
28502
28613
  return MAX_RESPONSE_SIZE_MESSAGE;
28503
28614
  }
28504
28615
  }
28505
- const text = yield config.text();
28616
+ const canStream = config.body && _.isFunction(config.body.getReader) && typeof TextDecoder !== 'undefined';
28617
+ const text = canStream ? yield this.readBodyStream(config.body) : yield config.text();
28506
28618
  if (text.length > MAX_RESPONSE_SIZE) {
28507
28619
  return MAX_RESPONSE_SIZE_MESSAGE;
28508
28620
  }
@@ -28513,6 +28625,26 @@ class NetworkRequestIntercept {
28513
28625
  }
28514
28626
  });
28515
28627
  }
28628
+ // Reads chunk by chunk so a long or never-ending stream stops buffering once it passes the cap
28629
+ readBodyStream(body) {
28630
+ return __awaiter(this, void 0, void 0, function* () {
28631
+ const reader = body.getReader();
28632
+ const decoder = new TextDecoder();
28633
+ let text = '';
28634
+ let bytesRead = 0;
28635
+ let chunk = yield reader.read();
28636
+ while (!chunk.done) {
28637
+ bytesRead += chunk.value.byteLength;
28638
+ if (bytesRead > MAX_RESPONSE_SIZE) {
28639
+ reader.cancel().catch(_.noop);
28640
+ return MAX_RESPONSE_SIZE_MESSAGE;
28641
+ }
28642
+ text += decoder.decode(chunk.value, { stream: true });
28643
+ chunk = yield reader.read();
28644
+ }
28645
+ return text + decoder.decode();
28646
+ });
28647
+ }
28516
28648
  extractConfigHeaders(config) {
28517
28649
  let headers = {};
28518
28650
  if (config.headers) {
@@ -28585,33 +28717,7 @@ class NetworkRequestIntercept {
28585
28717
  // Make the actual fetch call and capture response
28586
28718
  try {
28587
28719
  const res = yield networkInterceptor._originalFetch.apply(this, args);
28588
- // Clone the response to avoid consuming the body
28589
- const responseClone = res.clone();
28590
- try {
28591
- // Capture response data
28592
- const responseBody = yield networkInterceptor.safelyReadBody(responseClone, 'response');
28593
- const { status, statusText } = res;
28594
- const headers = networkInterceptor.extractConfigHeaders(res);
28595
- _.each(networkInterceptor.callbackFns, ({ response }) => {
28596
- if (_.isFunction(response)) {
28597
- response({
28598
- requestId,
28599
- status,
28600
- statusText,
28601
- body: responseBody,
28602
- headers,
28603
- url: transformedUrl,
28604
- method
28605
- });
28606
- }
28607
- });
28608
- }
28609
- catch (e) {
28610
- _.each(networkInterceptor.callbackFns, ({ error }) => {
28611
- if (_.isFunction(error))
28612
- error({ context: 'fetchResponse', error: e });
28613
- });
28614
- }
28720
+ networkInterceptor.captureResponse(res, { requestId, url: transformedUrl, method });
28615
28721
  return res;
28616
28722
  }
28617
28723
  catch (error) {
@@ -28635,6 +28741,29 @@ class NetworkRequestIntercept {
28635
28741
  });
28636
28742
  };
28637
28743
  }
28744
+ // Not awaited by the fetch wrapper: the app gets its response before the body is read
28745
+ captureResponse(res, { requestId, url, method }) {
28746
+ return __awaiter(this, void 0, void 0, function* () {
28747
+ try {
28748
+ // Clone synchronously, before the app can consume the body
28749
+ const responseClone = res.clone();
28750
+ const body = yield this.safelyReadBody(responseClone, 'response');
28751
+ const { status, statusText } = res;
28752
+ const headers = this.extractConfigHeaders(res);
28753
+ _.each(this.callbackFns, ({ response }) => {
28754
+ if (_.isFunction(response)) {
28755
+ response({ requestId, status, statusText, body, headers, url, method });
28756
+ }
28757
+ });
28758
+ }
28759
+ catch (e) {
28760
+ _.each(this.callbackFns, ({ error }) => {
28761
+ if (_.isFunction(error))
28762
+ error({ context: 'fetchResponse', error: e });
28763
+ });
28764
+ }
28765
+ });
28766
+ }
28638
28767
  patchXHR() {
28639
28768
  const networkInterceptor = this;
28640
28769
  networkInterceptor._originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
@@ -29638,7 +29767,8 @@ const PluginAPI = {
29638
29767
  transmit: {
29639
29768
  buildBaseDataUrl,
29640
29769
  fetchKeepalive,
29641
- sendBeacon
29770
+ sendBeacon,
29771
+ shouldLoadWithPost
29642
29772
  },
29643
29773
  log,
29644
29774
  NetworkRequest,
@@ -33817,10 +33947,12 @@ function Poll(guide) {
33817
33947
  _.each(responses, function (response, i) {
33818
33948
  var curPoll = getPoll(response.pollId, guide);
33819
33949
  var evt = createGuideEvent('pollResponse', step.guideId, step.id, get_visitor_id(), undefined, guide.language);
33950
+ // A behavior that replaces show() outright (HTML guides) drops the after('show')
33951
+ // hook above, so fall back to when the step was last seen.
33820
33952
  _.extend(evt.props, {
33821
33953
  poll_id: response.pollId,
33822
33954
  poll_response: response.value,
33823
- duration: (new Date().getTime() - shownTime)
33955
+ duration: (new Date().getTime() - (shownTime || step.lastSeenAt))
33824
33956
  }, options);
33825
33957
  if (curPoll &&
33826
33958
  curPoll.attributes &&
@@ -37132,7 +37264,11 @@ var Branding = (function () {
37132
37264
  return;
37133
37265
  var guide = pendo.findGuideById(props.guide_id);
37134
37266
  var guideContainer = guide.steps[0].guideElement[0].lastElementChild;
37135
- var verticalAlignment = guideContainer.dataset.verticalAlignment;
37267
+ var verticalAlignment = guideContainer && guideContainer.dataset.verticalAlignment;
37268
+ // Only Visual Design Studio containers carry the alignment the badge is placed by;
37269
+ // an HTML guide's container has none.
37270
+ if (!verticalAlignment)
37271
+ return;
37136
37272
  var isBottomAligned = verticalAlignment.indexOf('Bottom') > -1;
37137
37273
  var isSetWidth = guideContainer.style.width !== '';
37138
37274
  var link = document.createElement('a');
@@ -41066,7 +41202,7 @@ var buildLauncherItem = function (guide) {
41066
41202
  dom(guideContainer).attr('id', 'launcher-' + guide.id);
41067
41203
  var guideLink = document.createElement('a');
41068
41204
  guideLink.href = '#';
41069
- guideLink.innerHTML = guide.name;
41205
+ guideLink.appendChild(document.createTextNode(guide.name));
41070
41206
  guideContainer.appendChild(guideLink);
41071
41207
  return guideContainer;
41072
41208
  };
@@ -41752,7 +41888,7 @@ function SegmentFlags() {
41752
41888
  const jwtOptions = pluginApi.agent.getJwtInfoCopy();
41753
41889
  let payload;
41754
41890
  try {
41755
- if (url.length > pluginApi.constants.URL_MAX_LENGTH || !globalPendo._.isEmpty(jwtOptions)) {
41891
+ if (pluginApi.transmit.shouldLoadWithPost(url, jwtOptions)) {
41756
41892
  delete queryString.jzb;
41757
41893
  url = pluginApi.transmit.buildBaseDataUrl('segmentflag.json', globalPendo.apiKey, queryString);
41758
41894
  payload = globalPendo._.extend({ events: jzb }, jwtOptions);
@@ -42167,7 +42303,7 @@ class PromptPlugin {
42167
42303
  });
42168
42304
  const currentlyActiveIds = this._.map(this._.filter(this.prompts, prompt => prompt.isActive), prompt => prompt.id);
42169
42305
  this._.each(allAgents, aiAgent => {
42170
- if (this.isConversationAgent(aiAgent)) {
42306
+ if (this.isConversationAgent(aiAgent) || this.isNetworkAgent(aiAgent)) {
42171
42307
  return;
42172
42308
  }
42173
42309
  const isCurrentlyActive = this._.contains(currentlyActiveIds, aiAgent.id);
@@ -42192,30 +42328,21 @@ class PromptPlugin {
42192
42328
  isConversationAgent(agent) {
42193
42329
  return agent.preset && agent.preset != 'chatgpt';
42194
42330
  }
42331
+ // network tracking is not supported; these agents are ignored rather than falling back to DOM.
42332
+ isNetworkAgent(agent) {
42333
+ return agent.trackingMethod === 'network';
42334
+ }
42195
42335
  onConfigLoaded(aiAgents = []) {
42196
42336
  if (!aiAgents || !this._.isArray(aiAgents) || aiAgents.length === 0) {
42197
42337
  return;
42198
42338
  }
42199
- const networkAgents = [];
42200
42339
  const domAgents = [];
42201
42340
  for (const agent of aiAgents) {
42202
- if (this.isConversationAgent(agent)) {
42341
+ if (this.isConversationAgent(agent) || this.isNetworkAgent(agent)) {
42203
42342
  continue;
42204
42343
  }
42205
- if (agent.trackingMethod === 'network') {
42206
- networkAgents.push(agent);
42207
- }
42208
- else if (agent.trackingMethod === 'dom') {
42209
- domAgents.push(agent);
42210
- }
42211
- else {
42212
- // Default to DOM if no trackingMethod specified
42213
- domAgents.push(agent);
42214
- }
42215
- }
42216
- if (networkAgents.length > 0) {
42217
- this.networkAgents = networkAgents;
42218
- this.api.NetworkRequest.on({ request: this.handleRequest, error: this.handleError });
42344
+ // Default to DOM if no trackingMethod specified
42345
+ domAgents.push(agent);
42219
42346
  }
42220
42347
  // Cache agents for fast lookup
42221
42348
  if (!this.agentsCache) {
@@ -42236,29 +42363,6 @@ class PromptPlugin {
42236
42363
  }
42237
42364
  });
42238
42365
  }
42239
- handleRequest({ method, url, body }) {
42240
- const currentUrl = getNormalizedUrl();
42241
- this._.each(this.networkAgents, aiAgent => {
42242
- if (!this.testPageRule(aiAgent.pageRule, currentUrl, aiAgent.id)) {
42243
- return;
42244
- }
42245
- const originalBody = body || '';
42246
- this.api.analytics.collectEvent('prompt', {
42247
- prompt: originalBody,
42248
- agentId: aiAgent.id,
42249
- promptType: method,
42250
- url
42251
- }, undefined, 'agentic');
42252
- });
42253
- }
42254
- handleError({ error, context }) {
42255
- if (context == 'fetch') {
42256
- this.api.log.debug('[PromptPlugin] Fetch tracking failed:', error);
42257
- }
42258
- if (context == 'xhr') {
42259
- this.api.log.debug('[PromptPlugin] XHR tracking failed:', error);
42260
- }
42261
- }
42262
42366
  observePrompt(config) {
42263
42367
  const { id, cssSelectors } = config;
42264
42368
  // Check if agent already exists
@@ -42297,7 +42401,6 @@ class PromptPlugin {
42297
42401
  if (this.agentsCache) {
42298
42402
  this.agentsCache.clear();
42299
42403
  }
42300
- this.api.NetworkRequest.off({ request: this.handleRequest, error: this.handleError });
42301
42404
  }
42302
42405
  }
42303
42406
  var PromptAnalytics = new PromptPlugin();
@@ -43650,6 +43753,13 @@ function encodeMetadataForUrl(value, urlBeforePlaceholder = '') {
43650
43753
  }
43651
43754
  return window.encodeURI(str);
43652
43755
  }
43756
+ // Resolve through the browser's own URL parser: it strips tabs/newlines inside the scheme
43757
+ // (e.g. "java\tscript:"), which a regex over the raw string would miss.
43758
+ function hasDangerousUrlScheme(url) {
43759
+ const anchor = document.createElement('a');
43760
+ anchor.href = url;
43761
+ return DANGEROUS_URL_SCHEME.test(anchor.protocol);
43762
+ }
43653
43763
 
43654
43764
  const MetadataSubstitution = {
43655
43765
  name: 'MetadataSubstitution',
@@ -43736,27 +43846,37 @@ function processPlaceholder(placeholder, pendo) {
43736
43846
  let match;
43737
43847
  const { data, target } = placeholder;
43738
43848
  const subRegex = new RegExp(guideMarkdownUtil.substitutionRegex);
43849
+ const isUrlTarget = target === 'href' || target === 'value';
43850
+ const startedWithDangerousScheme = isUrlTarget && hasDangerousUrlScheme(getRawTargetValue(data, target));
43739
43851
  while ((match = matchPlaceholder(placeholder, subRegex))) {
43740
43852
  const usedArrayPath = Array.isArray(match) && match.length >= 2;
43741
43853
  const currentStr = target === 'textContent'
43742
43854
  ? data[target]
43743
- : decodeURI((data.getAttribute && (target === 'href' || target === 'value') ? (data.getAttribute(target) || '') : data[target]));
43855
+ : decodeURI(getRawTargetValue(data, target));
43744
43856
  const matched = usedArrayPath ? match : subRegex.exec(currentStr);
43745
43857
  if (!matched)
43746
43858
  continue;
43747
43859
  const mdValue = getSubstituteValue(matched, pendo);
43748
43860
  substituteMetadataByTarget(data, target, mdValue, matched);
43861
+ // Substituted values can carry new placeholders into the next pass, so a dangerous
43862
+ // scheme can be assembled across passes. Check the whole URL, not each fragment.
43863
+ if (isUrlTarget && !startedWithDangerousScheme && hasDangerousUrlScheme(getRawTargetValue(data, target))) {
43864
+ data[target] = '';
43865
+ return;
43866
+ }
43749
43867
  }
43750
43868
  }
43869
+ function getRawTargetValue(data, target) {
43870
+ return data.getAttribute && (target === 'href' || target === 'value')
43871
+ ? (data.getAttribute(target) || '')
43872
+ : data[target];
43873
+ }
43751
43874
  function matchPlaceholder(placeholder, regex) {
43752
43875
  if (placeholder.target === 'textContent') {
43753
43876
  return placeholder.data[placeholder.target].match(regex);
43754
43877
  }
43755
43878
  else {
43756
- const raw = placeholder.data.getAttribute && (placeholder.target === 'href' || placeholder.target === 'value')
43757
- ? (placeholder.data.getAttribute(placeholder.target) || '')
43758
- : placeholder.data[placeholder.target];
43759
- return decodeURI(raw).match(regex);
43879
+ return decodeURI(getRawTargetValue(placeholder.data, placeholder.target)).match(regex);
43760
43880
  }
43761
43881
  }
43762
43882
  function getMetadataValueCaseInsensitive(metadata, type, property) {
@@ -48001,7 +48121,7 @@ function serializeNodeWithId(n, options) {
48001
48121
  if (parent && isShadowRoot(parent) && isNativeShadowDom(parent)) {
48002
48122
  serializedNode.isShadow = true;
48003
48123
  }
48004
- if (serializedNode.type === NodeType.Element && serializedNode.tagName === "iframe") {
48124
+ if (serializedNode.type === NodeType.Element && serializedNode.tagName === "iframe" && !_isBlockedElement(n, blockClass, blockSelector)) {
48005
48125
  onceIframeLoaded(
48006
48126
  n,
48007
48127
  () => {
@@ -50151,11 +50271,15 @@ class IframeManager {
50151
50271
  __publicField(this, "pageHideListener");
50152
50272
  __publicField(this, "stylesheetManager");
50153
50273
  __publicField(this, "recordCrossOriginIframes");
50274
+ __publicField(this, "blockClass");
50275
+ __publicField(this, "blockSelector");
50154
50276
  this.mutationCb = options.mutationCb;
50155
50277
  this.wrappedEmit = options.wrappedEmit;
50156
50278
  this.takeFullSnapshot = options.takeFullSnapshot;
50157
50279
  this.stylesheetManager = options.stylesheetManager;
50158
50280
  this.recordCrossOriginIframes = options.recordCrossOriginIframes;
50281
+ this.blockClass = options.blockClass;
50282
+ this.blockSelector = options.blockSelector;
50159
50283
  this.crossOriginIframeStyleMirror = new CrossOriginIframeMirror(
50160
50284
  this.stylesheetManager.styleMirror.generateId.bind(
50161
50285
  this.stylesheetManager.styleMirror
@@ -50170,6 +50294,7 @@ class IframeManager {
50170
50294
  this.takeFullSnapshot = takeFullSnapshot2;
50171
50295
  }
50172
50296
  addIframe(iframeEl) {
50297
+ if (isBlocked(iframeEl, this.blockClass, this.blockSelector, false)) return;
50173
50298
  this.iframes.set(iframeEl, true);
50174
50299
  if (iframeEl.contentWindow)
50175
50300
  this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
@@ -51294,6 +51419,8 @@ function record(options = {}) {
51294
51419
  mutationCb: wrappedMutationEmit,
51295
51420
  stylesheetManager,
51296
51421
  recordCrossOriginIframes,
51422
+ blockClass,
51423
+ blockSelector,
51297
51424
  wrappedEmit,
51298
51425
  takeFullSnapshot
51299
51426
  });
@@ -53413,6 +53540,7 @@ function createReplayPlugin() {
53413
53540
  /**
53414
53541
  * limit of number of keys in an object
53415
53542
  * if an object contains more keys than this limit, we would call its toString function directly
53543
+ * arrays longer than this limit are cut down to their first NUM_OF_KEYS_LIMIT elements
53416
53544
  */
53417
53545
  const NUM_OF_KEYS_LIMIT = 50;
53418
53546
  /**
@@ -53484,8 +53612,9 @@ function shouldIgnore(_obj) {
53484
53612
  /**
53485
53613
  * stringify any argument
53486
53614
  * @param arg - the arg to stringify
53615
+ * @param maxLength - stop serializing once this many characters have been visited
53487
53616
  */
53488
- function stringify(arg) {
53617
+ function stringify(arg, maxLength = Infinity) {
53489
53618
  if (arg === undefined)
53490
53619
  return 'undefined';
53491
53620
  // Handle primitives
@@ -53496,11 +53625,19 @@ function stringify(arg) {
53496
53625
  return arg.toString();
53497
53626
  return String(arg);
53498
53627
  }
53628
+ let charsVisited = 0;
53499
53629
  return JSON.stringify(arg, function (key, value) {
53630
+ if (charsVisited > maxLength)
53631
+ return '';
53632
+ const valueLength = typeof value === 'string' ? value.length : 0;
53633
+ charsVisited += key.length + valueLength;
53500
53634
  if (value === null)
53501
53635
  return value;
53502
53636
  if (value === undefined)
53503
53637
  return 'undefined';
53638
+ if (Array.isArray(value) && value.length > NUM_OF_KEYS_LIMIT) {
53639
+ return value.slice(0, NUM_OF_KEYS_LIMIT);
53640
+ }
53504
53641
  // Handle objects
53505
53642
  if (shouldIgnore(value)) {
53506
53643
  return value.toString ? value.toString() : '[object Object]';
@@ -54127,8 +54264,11 @@ function ConsoleCapture() {
54127
54264
  let isRecordingActive = false;
54128
54265
  const CAPTURE_CONSOLE_CONFIG = 'captureConsoleLogs';
54129
54266
  const ALWAYS_CAPTURE_ERRORS_CONFIG = 'alwaysCaptureDevLogErrors';
54267
+ const CONSOLE_LOG_LEVELS_CONFIG = 'consoleLogLevels';
54130
54268
  const CONSOLE_METHODS = ['log', 'warn', 'error', 'info'];
54131
54269
  const DEV_LOG_SUB_TYPE = 'console';
54270
+ const DEFAULT_CONSOLE_LOG_LEVELS = ['error', 'warn', 'info'];
54271
+ let consoleLogLevels = DEFAULT_CONSOLE_LOG_LEVELS;
54132
54272
  // deduplicate logs
54133
54273
  let lastLogKey = '';
54134
54274
  return {
@@ -54147,6 +54287,8 @@ function ConsoleCapture() {
54147
54287
  onPtmPaused,
54148
54288
  onPtmUnpaused,
54149
54289
  securityPolicyViolationFn,
54290
+ uncaughtErrorFn,
54291
+ unhandledRejectionFn,
54150
54292
  get isCapturingConsoleLogs() {
54151
54293
  return isCapturingConsoleLogs;
54152
54294
  },
@@ -54166,10 +54308,12 @@ function ConsoleCapture() {
54166
54308
  const { ConfigReader } = pluginAPI;
54167
54309
  ConfigReader.addOption(CAPTURE_CONSOLE_CONFIG, [ConfigReader.sources.PENDO_CONFIG_SRC], false);
54168
54310
  ConfigReader.addOption(ALWAYS_CAPTURE_ERRORS_CONFIG, [ConfigReader.sources.PENDO_CONFIG_SRC], false);
54311
+ ConfigReader.addOption(CONSOLE_LOG_LEVELS_CONFIG, [ConfigReader.sources.PENDO_CONFIG_SRC], DEFAULT_CONSOLE_LOG_LEVELS);
54169
54312
  const captureConsoleEnabled = ConfigReader.get(CAPTURE_CONSOLE_CONFIG);
54170
54313
  if (!captureConsoleEnabled)
54171
54314
  return;
54172
54315
  alwaysCaptureErrors = !!ConfigReader.get(ALWAYS_CAPTURE_ERRORS_CONFIG);
54316
+ consoleLogLevels = ConfigReader.get(CONSOLE_LOG_LEVELS_CONFIG);
54173
54317
  globalPendo = pendo;
54174
54318
  buffer = new DevlogBuffer(pendo, pluginAPI);
54175
54319
  transport = new DevlogTransport(pendo, pluginAPI);
@@ -54230,6 +54374,8 @@ function ConsoleCapture() {
54230
54374
  try {
54231
54375
  addIntercepts();
54232
54376
  pluginAPI.attachEventInternal(window, 'securitypolicyviolation', securityPolicyViolationFn);
54377
+ pluginAPI.attachEventInternal(window, 'error', uncaughtErrorFn);
54378
+ pluginAPI.attachEventInternal(window, 'unhandledrejection', unhandledRejectionFn);
54233
54379
  }
54234
54380
  catch (error) {
54235
54381
  pluginAPI.log.error('Error setting up console capture intercepts: ', error);
@@ -54270,16 +54416,36 @@ function ConsoleCapture() {
54270
54416
  createConsoleEvent([message], 'error', { skipStackTrace: true, skipScrubPII: true });
54271
54417
  }
54272
54418
  }
54419
+ function uncaughtErrorFn(evt) {
54420
+ if (!evt || typeof evt !== 'object')
54421
+ return;
54422
+ const rawMessage = evt.message || (evt.error && String(evt.error));
54423
+ if (!rawMessage)
54424
+ return;
54425
+ const message = rawMessage.indexOf('Uncaught') === 0 ? rawMessage : `Uncaught ${rawMessage}`;
54426
+ const stackTrace = evt.error && evt.error.stack;
54427
+ createConsoleEvent([stackTrace ? `${message}\n${stackTrace}` : message], 'error', { skipStackTrace: true });
54428
+ }
54429
+ function unhandledRejectionFn(evt) {
54430
+ if (!evt || typeof evt !== 'object')
54431
+ return;
54432
+ const { reason } = evt;
54433
+ const detail = reason && reason.stack ? reason.stack : reason;
54434
+ createConsoleEvent(['Uncaught (in promise)', detail], 'error', { skipStackTrace: true });
54435
+ }
54273
54436
  function createConsoleEvent(args, methodName, { skipStackTrace = false, skipScrubPII = false } = {}) {
54274
54437
  if (!isCapturingConsoleLogs || !args || args.length === 0 || !_.contains(CONSOLE_METHODS, methodName))
54275
54438
  return;
54276
54439
  if (isErrorOnlyCapture() && methodName !== 'error')
54277
54440
  return;
54441
+ const devLogLevel = methodName === 'log' ? 'info' : methodName;
54442
+ if (!_.contains(consoleLogLevels, devLogLevel))
54443
+ return;
54278
54444
  // stringify args
54279
54445
  let message = _.compact(_.map(args, arg => {
54280
54446
  let stringifiedArg;
54281
54447
  try {
54282
- stringifiedArg = stringify(arg);
54448
+ stringifiedArg = stringify(arg, MAX_LENGTH);
54283
54449
  }
54284
54450
  catch (error) {
54285
54451
  pluginAPI.log.error('Error stringifying arg: ', error);
@@ -54303,10 +54469,10 @@ function ConsoleCapture() {
54303
54469
  buffer.lastEvent.devLogCount++;
54304
54470
  return;
54305
54471
  }
54306
- const devLogEnvelope = createDevLogEnvelope(pluginAPI, globalPendo);
54307
- const consoleEvent = Object.assign(Object.assign({}, devLogEnvelope), { subType: DEV_LOG_SUB_TYPE, devLogLevel: methodName === 'log' ? 'info' : methodName, devLogMessage: skipScrubPII ? message : scrubPII({ string: message, _ }), devLogTrace: stackTrace, devLogCount: 1 });
54308
54472
  if (!buffer.hasTokenAvailable())
54309
- return consoleEvent;
54473
+ return;
54474
+ const devLogEnvelope = createDevLogEnvelope(pluginAPI, globalPendo);
54475
+ const consoleEvent = Object.assign(Object.assign({}, devLogEnvelope), { subType: DEV_LOG_SUB_TYPE, devLogLevel, devLogMessage: skipScrubPII ? message : scrubPII({ string: message, _ }), devLogTrace: stackTrace, devLogCount: 1 });
54310
54476
  if (!isPtmPaused) {
54311
54477
  pluginAPI.Events.eventCaptured.trigger(consoleEvent);
54312
54478
  }
@@ -54364,6 +54530,8 @@ function ConsoleCapture() {
54364
54530
  pluginAPI.Events['recording:unpaused'].off(recordingStarted);
54365
54531
  pluginAPI.Events['recording:paused'].off(recordingStopped);
54366
54532
  pluginAPI.detachEventInternal(window, 'securitypolicyviolation', securityPolicyViolationFn);
54533
+ pluginAPI.detachEventInternal(window, 'error', uncaughtErrorFn);
54534
+ pluginAPI.detachEventInternal(window, 'unhandledrejection', unhandledRejectionFn);
54367
54535
  _.each(CONSOLE_METHODS, function (methodName) {
54368
54536
  if (!console[methodName])
54369
54537
  return _.noop;