@pendo/agent 2.293.0 → 2.294.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.
@@ -438,7 +438,7 @@ function loadAsModule(config) {
438
438
  }
439
439
  function createPendoObject(config) {
440
440
  var windowOrMountPoint = loadAsModule(config) ? {} : window;
441
- const agentScriptTag = findAgentScriptTag(document.getElementsByTagName('script'), config.apiKey);
441
+ const agentScriptTag = document.currentScript || findAgentScriptTag(document.getElementsByTagName('script'), config.apiKey);
442
442
  let globalKey = config.pendoGlobalKey || 'pendo';
443
443
  if (agentScriptTag) {
444
444
  globalKey = agentScriptTag.getAttribute('data-pendo-global-key') || globalKey;
@@ -463,9 +463,6 @@ function shouldUseUnminifiedAgent(config, debuggingEnabled) {
463
463
  return !isExtension(config) && isMinifiedAgent(config) && debuggingEnabled;
464
464
  }
465
465
  function findAgentScriptTag(scripts = [], apiKey) {
466
- if (document.currentScript) {
467
- return document.currentScript;
468
- }
469
466
  const regex = /^https:\/\/[\w\-.]*cdn[\w\-.]*\.(pendo-dev\.com|pendo\.io)\/agent\/static\/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12}|PENDO_API_KEY)\/pendo\.js$/g;
470
467
  for (let i = 0; i < scripts.length; i++) {
471
468
  const script = scripts[i];
@@ -3911,8 +3908,8 @@ var SERVER = '';
3911
3908
  var ASSET_HOST = '';
3912
3909
  var ASSET_PATH = '';
3913
3910
  var DESIGNER_SERVER = '';
3914
- var VERSION = '2.293.0_';
3915
- var PACKAGE_VERSION = '2.293.0';
3911
+ var VERSION = '2.294.0_';
3912
+ var PACKAGE_VERSION = '2.294.0';
3916
3913
  var LOADER = 'xhr';
3917
3914
  /* eslint-enable agent-eslint-rules/no-gulp-env-references */
3918
3915
  /**
@@ -12234,6 +12231,7 @@ class PerformanceMonitor {
12234
12231
  return detectNativeBrowserAPI('performance.mark') &&
12235
12232
  detectNativeBrowserAPI('performance.measure') &&
12236
12233
  detectNativeBrowserAPI('performance.getEntries') &&
12234
+ detectNativeBrowserAPI('performance.getEntriesByName') &&
12237
12235
  detectNativeBrowserAPI('performance.clearMarks') &&
12238
12236
  detectNativeBrowserAPI('performance.clearMeasures');
12239
12237
  }
@@ -12253,8 +12251,10 @@ class PerformanceMonitor {
12253
12251
  name = `pendo-${name}`;
12254
12252
  const startMark = `${name}-start`;
12255
12253
  const stopMark = `${name}-stop`;
12256
- performance.measure(name, startMark, stopMark);
12257
- this._count(this._measures, name);
12254
+ if (performance.getEntriesByName(startMark).length && performance.getEntriesByName(stopMark).length) {
12255
+ performance.measure(name, startMark, stopMark);
12256
+ this._count(this._measures, name);
12257
+ }
12258
12258
  }
12259
12259
  _count(registry, name) {
12260
12260
  if (!registry[name]) {
@@ -13307,6 +13307,7 @@ var getValidTarget = function (node) {
13307
13307
  */
13308
13308
  var handle_event = function (evt) {
13309
13309
  try {
13310
+ PerformanceMonitor$1.startTimer('event-captured');
13310
13311
  if (dom.data.get(evt, 'counted'))
13311
13312
  return;
13312
13313
  dom.data.set(evt, 'counted', true);
@@ -13346,6 +13347,9 @@ var handle_event = function (evt) {
13346
13347
  catch (e) {
13347
13348
  log.critical('pendo.io while handling event', { error: e });
13348
13349
  }
13350
+ finally {
13351
+ PerformanceMonitor$1.stopTimer('event-captured');
13352
+ }
13349
13353
  };
13350
13354
  function getClickEventProperties(target) {
13351
13355
  const eventPropertyHandler = getEventPropertyHandler(target);
@@ -17869,6 +17873,9 @@ function getAllowedAttributes(attributeKeyValueMap, stepId, guideId, type) {
17869
17873
  function buildNodeFromJSON(json, step, guides) {
17870
17874
  step = step || { id: 'unknown', guideId: 'unknown' };
17871
17875
  json.props = getAllowedAttributes(json.props, step.id, step.guideId, json.type);
17876
+ if (step.isDarkMode && json.darkModeProps) {
17877
+ json.darkModeProps = getAllowedAttributes(json.darkModeProps, step.id, step.guideId, json.type);
17878
+ }
17872
17879
  var curNode = dom(document.createElement(json.type));
17873
17880
  // APP-81040 calling code in the pendo app (and possibly elsewhere) depends on
17874
17881
  // curNode.getParent() returning non-null in some cases. The fact that it used to
@@ -17888,7 +17895,11 @@ function buildNodeFromJSON(json, step, guides) {
17888
17895
  }
17889
17896
  _.each(json.props, function (propValue, propKey) {
17890
17897
  if (propKey === 'style') {
17891
- curNode.css(json.props.style);
17898
+ let nodeStyle = json.props.style;
17899
+ if (step.isDarkMode && json.darkModeProps) {
17900
+ nodeStyle = Object.assign(Object.assign({}, json.props.style), json.darkModeProps.style);
17901
+ }
17902
+ curNode.css(nodeStyle);
17892
17903
  }
17893
17904
  else if (propKey === 'data-pendo-code-block' && propValue === true && !ConfigReader.get('preventCodeInjection')) {
17894
17905
  const htmlString = step.getContent();
@@ -17913,13 +17924,23 @@ function buildNodeFromJSON(json, step, guides) {
17913
17924
  if (nonce) {
17914
17925
  curNode.attr('nonce', nonce);
17915
17926
  }
17927
+ let css = json.css;
17928
+ if (json.forDarkMode) {
17929
+ const darkModeSelector = _.get(step, 'attributes.darkMode.selector', '');
17930
+ css = _.map(css, function (rule) {
17931
+ return {
17932
+ styles: rule.styles,
17933
+ selector: `${darkModeSelector} ${rule.selector}`
17934
+ };
17935
+ });
17936
+ }
17916
17937
  // TODO: make this render building-block pseudo-styles properly for IE7-8. This current functionality allows guides to render in IE but there are lots of styling problems.
17917
17938
  // `curNode.text` specifically breaks in IE8 since style tags text attributes are read only. From researching `node.styleSheet.cssText` is the correct way to do it.
17918
17939
  if (curNode.styleSheet) {
17919
- curNode.styleSheet.cssText = buildStyleTagContent(json.css);
17940
+ curNode.styleSheet.cssText = buildStyleTagContent(css);
17920
17941
  }
17921
17942
  else {
17922
- curNode.text(buildStyleTagContent(json.css));
17943
+ curNode.text(buildStyleTagContent(css));
17923
17944
  }
17924
17945
  }
17925
17946
  if (json.svgWidgetId) {
@@ -20617,6 +20638,21 @@ class AutoDisplayPhase {
20617
20638
  }
20618
20639
  }
20619
20640
 
20641
+ function syncColorMode(step, rerender = false) {
20642
+ const darkModeSelector = _.get(step, 'attributes.darkMode.selector', '');
20643
+ if (!darkModeSelector)
20644
+ return;
20645
+ const isDarkMode = pendo$1.Sizzle(darkModeSelector).length > 0;
20646
+ const darkModeChanged = step.isDarkMode !== isDarkMode;
20647
+ if (darkModeChanged) {
20648
+ step.isDarkMode = isDarkMode;
20649
+ if (rerender) {
20650
+ step.hide();
20651
+ step.show(step.seenReason);
20652
+ }
20653
+ }
20654
+ }
20655
+
20620
20656
  /*
20621
20657
  * Guide Loop
20622
20658
  *
@@ -20815,6 +20851,7 @@ function stepShowingProc(guide, step) {
20815
20851
  step.attributes.currentTextZoomFontSize = currentBrowserFontSize;
20816
20852
  }
20817
20853
  }
20854
+ syncColorMode(step, true);
20818
20855
  if (step.elementPathRule && targetElement && !SizzleProxy.matchesSelector(targetElement, step.elementPathRule)) {
20819
20856
  step.hide();
20820
20857
  return;
@@ -26343,53 +26380,152 @@ class NetworkRequestIntercept {
26343
26380
  extractXHRHeaders(xhr) {
26344
26381
  return xhr[PENDO_HEADERS_KEY] || {};
26345
26382
  }
26346
- patchNetwork() {
26383
+ generateRequestId() {
26384
+ const $stringLength = 8;
26385
+ return 'req_' + Date.now() + '_' + pendo$1.randomString($stringLength);
26386
+ }
26387
+ safelyReadResponse(response) {
26388
+ return __awaiter(this, void 0, void 0, function* () {
26389
+ try {
26390
+ const contentType = response.headers.get('content-type') || '';
26391
+ if (contentType.indexOf('application/json') !== -1) {
26392
+ return yield response.json();
26393
+ }
26394
+ else if (contentType.indexOf('text/') !== -1) {
26395
+ return yield response.text();
26396
+ }
26397
+ else {
26398
+ // For binary or unknown content types, just capture metadata
26399
+ return `[Binary content: ${contentType}]`;
26400
+ }
26401
+ }
26402
+ catch (e) {
26403
+ return '[Unable to read response body]';
26404
+ }
26405
+ });
26406
+ }
26407
+ extractResponseHeaders(response) {
26408
+ let headers = {};
26409
+ if (response.headers) {
26410
+ const headerEntries = Array.from(response.headers.entries());
26411
+ headers = this.entriesToObject(headerEntries);
26412
+ }
26413
+ return headers;
26414
+ }
26415
+ parseXHRResponseHeaders(headerString) {
26416
+ const headers = {};
26417
+ if (!headerString)
26418
+ return headers;
26419
+ const lines = headerString.split('\r\n');
26420
+ _.each(lines, line => {
26421
+ const parts = line.split(': ');
26422
+ if (parts.length === 2) {
26423
+ headers[parts[0]] = parts[1];
26424
+ }
26425
+ });
26426
+ return headers;
26427
+ }
26428
+ patchFetch() {
26347
26429
  const networkInterceptor = this;
26348
- if (networkInterceptor._networkPatched)
26349
- return;
26350
- networkInterceptor._networkPatched = true;
26351
- networkInterceptor._originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
26352
- XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
26353
- this[PENDO_HEADERS_KEY] = this[PENDO_HEADERS_KEY] || {};
26354
- this[PENDO_HEADERS_KEY][header] = value;
26355
- return networkInterceptor._originalSetRequestHeader.apply(this, arguments);
26356
- };
26357
26430
  networkInterceptor._originalFetch = window.fetch;
26358
26431
  window.fetch = function (...args) {
26359
26432
  return __awaiter(this, void 0, void 0, function* () {
26360
26433
  const [url, config = {}] = args;
26361
26434
  const method = config.method || 'GET';
26435
+ const requestId = networkInterceptor.generateRequestId();
26436
+ // Capture request data
26362
26437
  try {
26363
26438
  const headers = networkInterceptor.extractHeaders(config.headers);
26364
26439
  const body = networkInterceptor.safelySerializeBody(config.body);
26365
26440
  _.each(networkInterceptor.callbackFns, ({ request }) => {
26366
26441
  if (_.isFunction(request))
26367
- request({ method, url, body, headers });
26442
+ request({ requestId, method, url, body, headers });
26368
26443
  });
26369
26444
  }
26370
26445
  catch (e) {
26371
26446
  _.each(networkInterceptor.callbackFns, ({ error }) => {
26372
26447
  if (_.isFunction(error))
26373
- error({ context: 'fetch', error: e });
26448
+ error({ context: 'fetchRequest', error: e });
26449
+ });
26450
+ }
26451
+ // Make the actual fetch call and capture response
26452
+ try {
26453
+ const res = yield networkInterceptor._originalFetch.apply(this, args);
26454
+ // Clone the response to avoid consuming the body
26455
+ const responseClone = res.clone();
26456
+ try {
26457
+ // Capture response data
26458
+ const responseBody = yield networkInterceptor.safelyReadResponse(responseClone);
26459
+ const { status, statusText } = res;
26460
+ const headers = networkInterceptor.extractResponseHeaders(res);
26461
+ _.each(networkInterceptor.callbackFns, ({ response }) => {
26462
+ if (_.isFunction(response)) {
26463
+ response({
26464
+ requestId,
26465
+ status,
26466
+ statusText,
26467
+ body: responseBody,
26468
+ headers,
26469
+ url,
26470
+ method
26471
+ });
26472
+ }
26473
+ });
26474
+ }
26475
+ catch (e) {
26476
+ _.each(networkInterceptor.callbackFns, ({ error }) => {
26477
+ if (_.isFunction(error))
26478
+ error({ context: 'fetchResponse', error: e });
26479
+ });
26480
+ }
26481
+ return res;
26482
+ }
26483
+ catch (error) {
26484
+ // Handle network errors
26485
+ _.each(networkInterceptor.callbackFns, ({ response }) => {
26486
+ if (_.isFunction(response)) {
26487
+ response({
26488
+ requestId,
26489
+ status: 0,
26490
+ statusText: 'Network Error',
26491
+ headers: {},
26492
+ body: null,
26493
+ url,
26494
+ method,
26495
+ error: error.message
26496
+ });
26497
+ }
26374
26498
  });
26499
+ throw error;
26375
26500
  }
26376
- return networkInterceptor._originalFetch.apply(this, args);
26377
26501
  });
26378
26502
  };
26503
+ }
26504
+ patchXHR() {
26505
+ const networkInterceptor = this;
26506
+ networkInterceptor._originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
26379
26507
  networkInterceptor._originalXHROpen = XMLHttpRequest.prototype.open;
26380
26508
  networkInterceptor._originalXHRSend = XMLHttpRequest.prototype.send;
26509
+ XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
26510
+ this[PENDO_HEADERS_KEY] = this[PENDO_HEADERS_KEY] || {};
26511
+ this[PENDO_HEADERS_KEY][header] = value;
26512
+ return networkInterceptor._originalSetRequestHeader.apply(this, arguments);
26513
+ };
26381
26514
  XMLHttpRequest.prototype.open = function (method, url) {
26382
- this._method = method; // Store data per XHR instance
26515
+ this._method = method;
26383
26516
  this._url = url;
26517
+ this._requestId = networkInterceptor.generateRequestId();
26384
26518
  return networkInterceptor._originalXHROpen.apply(this, arguments);
26385
26519
  };
26386
26520
  XMLHttpRequest.prototype.send = function (body) {
26521
+ const xhr = this;
26522
+ const headers = networkInterceptor.extractXHRHeaders(this);
26523
+ const safeBody = networkInterceptor.safelySerializeBody(body);
26387
26524
  try {
26388
- const headers = networkInterceptor.extractXHRHeaders(this);
26389
- const safeBody = networkInterceptor.safelySerializeBody(body);
26390
26525
  _.each(networkInterceptor.callbackFns, ({ request }) => {
26391
26526
  if (_.isFunction(request)) {
26392
26527
  request({
26528
+ requestId: this._requestId,
26393
26529
  method: this._method || 'GET',
26394
26530
  url: this._url,
26395
26531
  body: safeBody,
@@ -26401,7 +26537,7 @@ class NetworkRequestIntercept {
26401
26537
  catch (e) {
26402
26538
  _.each(networkInterceptor.callbackFns, ({ error }) => {
26403
26539
  if (_.isFunction(error))
26404
- error({ context: 'xhr', error: e });
26540
+ error({ context: 'xhrRequest', error: e });
26405
26541
  });
26406
26542
  }
26407
26543
  // Clean up custom headers after the request completes
@@ -26410,9 +26546,49 @@ class NetworkRequestIntercept {
26410
26546
  delete this[PENDO_HEADERS_KEY];
26411
26547
  }, { once: true });
26412
26548
  }
26549
+ // Set up response tracking
26550
+ const originalOnReadyStateChange = xhr.onreadystatechange;
26551
+ xhr.onreadystatechange = function () {
26552
+ if (xhr.readyState === 4) { // Request completed
26553
+ try {
26554
+ const headers = networkInterceptor.parseXHRResponseHeaders(xhr.getAllResponseHeaders());
26555
+ const { status, statusText, responseText: body, _url } = xhr;
26556
+ _.each(networkInterceptor.callbackFns, ({ response }) => {
26557
+ if (_.isFunction(response)) {
26558
+ response({
26559
+ requestId: xhr._requestId,
26560
+ status,
26561
+ statusText,
26562
+ headers,
26563
+ body,
26564
+ url: _url,
26565
+ method: xhr._method || 'GET'
26566
+ });
26567
+ }
26568
+ });
26569
+ }
26570
+ catch (e) {
26571
+ _.each(networkInterceptor.callbackFns, ({ error }) => {
26572
+ if (_.isFunction(error))
26573
+ error({ context: 'xhrResponse', error: e });
26574
+ });
26575
+ }
26576
+ }
26577
+ if (originalOnReadyStateChange) {
26578
+ originalOnReadyStateChange.apply(this, arguments);
26579
+ }
26580
+ };
26413
26581
  return networkInterceptor._originalXHRSend.apply(this, arguments);
26414
26582
  };
26415
26583
  }
26584
+ patchNetwork() {
26585
+ const networkInterceptor = this;
26586
+ if (networkInterceptor._networkPatched)
26587
+ return;
26588
+ networkInterceptor._networkPatched = true;
26589
+ networkInterceptor.patchFetch();
26590
+ networkInterceptor.patchXHR();
26591
+ }
26416
26592
  on({ request, response, error } = {}) {
26417
26593
  if (!this.callbackFns) {
26418
26594
  this.callbackFns = [];
@@ -26420,7 +26596,7 @@ class NetworkRequestIntercept {
26420
26596
  if (_.isFunction(request) || _.isFunction(response) || _.isFunction(error)) {
26421
26597
  this.callbackFns.push({ request, response, error });
26422
26598
  }
26423
- if (!this._networkPatched && !this._patching) {
26599
+ if (!this._networkPatched) {
26424
26600
  this.patchNetwork();
26425
26601
  }
26426
26602
  return this;
@@ -27240,6 +27416,8 @@ const PluginAPI = {
27240
27416
  collectEvent
27241
27417
  },
27242
27418
  attachEvent,
27419
+ attachEventInternal,
27420
+ detachEventInternal,
27243
27421
  ConfigReader,
27244
27422
  Events,
27245
27423
  EventTracer,
@@ -28204,6 +28382,9 @@ var BuildingBlockGuides = (function () {
28204
28382
  function renderGuideFromJSON(json, step, guides, options) {
28205
28383
  options = options || {};
28206
28384
  var guide = step.getGuide();
28385
+ if (step.isDarkMode === undefined) {
28386
+ syncColorMode(step);
28387
+ }
28207
28388
  var containerJSON = findGuideContainerJSON(json);
28208
28389
  var isResourceCenter = _.get(guide, 'attributes.resourceCenter');
28209
28390
  if (isResourceCenter) {
@@ -31071,6 +31252,10 @@ var GuideDisplay = (function () {
31071
31252
  if (step.type !== 'whatsnew' && !step.isShown()) {
31072
31253
  return false;
31073
31254
  }
31255
+ if (isGuideRequestPending()) {
31256
+ log.info('guides are loading.', { contexts: ['guides', 'loading'] });
31257
+ return false;
31258
+ }
31074
31259
  step.unlock();
31075
31260
  step._show(reason);
31076
31261
  return step.isShown();
@@ -31555,6 +31740,7 @@ function GuideStep(guide) {
31555
31740
  step.seenState = 'active';
31556
31741
  setSeenTime(getNow());
31557
31742
  var seenProps = {
31743
+ color_mode: step.isDarkMode ? 'dark' : 'default',
31558
31744
  last_updated_at: step.lastUpdatedAt
31559
31745
  };
31560
31746
  var pollTypes = this.getStepPollTypes(guide, step);
@@ -33246,7 +33432,7 @@ const DebuggerModule = (() => {
33246
33432
  location: { type: SYNC_TYPES.BOTTOM_UP, defaultValue: {} }
33247
33433
  };
33248
33434
  const actions = {
33249
- init: (context) => {
33435
+ init: (context, buffer) => {
33250
33436
  context.commit('setFrameId', EventTracer.getFrameId());
33251
33437
  context.commit('setTabId', EventTracer.getTabId());
33252
33438
  context.commit('setInstallType', getInstallType());
@@ -33266,6 +33452,7 @@ const DebuggerModule = (() => {
33266
33452
  frameId: context.state.frameId
33267
33453
  });
33268
33454
  }
33455
+ _.each(buffer.events, (event) => eventCapturedFn(event));
33269
33456
  },
33270
33457
  join: (context, data) => {
33271
33458
  if (context.state.frameId === data.frameId)
@@ -36959,12 +37146,18 @@ function disableDebugging() {
36959
37146
  _.extend(debug, debugging);
36960
37147
 
36961
37148
  const waitForLeader = (Events, store, q) => {
37149
+ const buffer = {
37150
+ events: []
37151
+ };
37152
+ Events.on('eventCaptured', (evt) => {
37153
+ buffer.events.push(evt);
37154
+ });
36962
37155
  if (store.getters['frames/leaderExists']()) {
36963
- return q.resolve();
37156
+ return q.resolve(buffer);
36964
37157
  }
36965
37158
  const deferred = q.defer();
36966
37159
  Events.on('leaderChanged', () => {
36967
- deferred.resolve();
37160
+ deferred.resolve(buffer);
36968
37161
  });
36969
37162
  return deferred.promise;
36970
37163
  };
@@ -36979,8 +37172,8 @@ const DebuggerLauncher = (function () {
36979
37172
  function initialize(pendo, PluginAPI) {
36980
37173
  const { Events, q } = PluginAPI;
36981
37174
  store = PluginAPI.store;
36982
- waitForLeader(Events, store, q).then(() => {
36983
- store.dispatch('debugger/init');
37175
+ waitForLeader(Events, store, q).then((buffer) => {
37176
+ store.dispatch('debugger/init', buffer);
36984
37177
  if (store.getters['frames/isLeader']()) {
36985
37178
  startDebuggingModuleIfEnabled();
36986
37179
  }
@@ -44346,13 +44539,15 @@ class MutationBuffer {
44346
44539
  index.childNodes(textarea),
44347
44540
  (cn) => index.textContent(cn) || ""
44348
44541
  ).join("");
44542
+ const needsMask = needMaskingText(textarea, this.maskTextClass, this.maskTextSelector, true);
44349
44543
  item.attributes.value = maskInputValue({
44350
44544
  element: textarea,
44351
44545
  maskInputOptions: this.maskInputOptions,
44352
44546
  tagName: textarea.tagName,
44353
44547
  type: getInputType(textarea),
44354
44548
  value,
44355
- maskInputFn: this.maskInputFn
44549
+ maskInputFn: this.maskInputFn,
44550
+ needsMask
44356
44551
  });
44357
44552
  });
44358
44553
  __publicField(this, "processMutation", (m) => {
@@ -52558,6 +52753,12 @@ class SessionRecorder {
52558
52753
  return;
52559
52754
  clearTimeout(this._changeIdentityTimer);
52560
52755
  if (this.isRecording()) {
52756
+ this.logStopReason('IDENTITY_CHANGED', {
52757
+ previousVisitorId: this.visitorId,
52758
+ newVisitorId: identifyEvent.data[0].visitor_id,
52759
+ previousAccountId: this.accountId,
52760
+ newAccountId: identifyEvent.data[0].account_id
52761
+ });
52561
52762
  this.stop();
52562
52763
  }
52563
52764
  this.visitorId = identifyEvent.data[0].visitor_id;
@@ -52653,7 +52854,7 @@ class SessionRecorder {
52653
52854
  this._start();
52654
52855
  }
52655
52856
  _markEvents(events) {
52656
- if (!this.recordingId)
52857
+ if (!this.recordingId || !this.isRecording())
52657
52858
  return;
52658
52859
  for (var e of events) {
52659
52860
  if ((e.visitor_id === this.visitorId || e.visitorId === this.visitorId) && e.type !== 'identify') {
@@ -52971,6 +53172,8 @@ class SessionRecorder {
52971
53172
  }
52972
53173
  }
52973
53174
  addRecordingId(event) {
53175
+ if (!this.isRecording())
53176
+ return;
52974
53177
  if (!this.recordingId || !event || !event.data || !event.data.length)
52975
53178
  return;
52976
53179
  var capturedEvent = event.data[0];
@@ -52979,6 +53182,10 @@ class SessionRecorder {
52979
53182
  if (capturedEvent.visitor_id !== this.visitorId && capturedEvent.visitorId !== this.visitorId) {
52980
53183
  // visitor id has already diverged from the agent, we'll stop sending events and just return here
52981
53184
  this.api.log.warn('Visitor id has diverged from agent');
53185
+ this.logStopReason('VISITOR_ID_DIVERGED', {
53186
+ agentVisitorId: capturedEvent.visitor_id || capturedEvent.visitorId,
53187
+ recordingVisitorId: this.visitorId
53188
+ });
52982
53189
  this.stop();
52983
53190
  return;
52984
53191
  }
@@ -54158,6 +54365,168 @@ function scrubPII(string) {
54158
54365
  return Object.values(PII_PATTERN).reduce((str, pattern) => str.replace(pattern, PII_REPLACEMENT), string);
54159
54366
  }
54160
54367
 
54368
+ /**
54369
+ * Determines the type of resource that was blocked based on the blocked URI and CSP directive.
54370
+ *
54371
+ * @param {string} blockedURI - The URI that was blocked by the CSP policy (can be 'inline', 'eval', or a URL)
54372
+ * @param {string} directive - The CSP directive that caused the violation (e.g., 'script-src', 'style-src')
54373
+ * @returns {string} A human-readable description of the resource type
54374
+ *
54375
+ * @example
54376
+ * getResourceType('inline', 'script-src') // returns 'inline script'
54377
+ * getResourceType('https://example.com/script.js', 'script-src') // returns 'script'
54378
+ * getResourceType('https://example.com/image.jpg', 'img-src') // returns 'image'
54379
+ * getResourceType('https://example.com/worker.js', 'worker-src') // returns 'worker'
54380
+ */
54381
+ function getResourceType(blockedURI, directive) {
54382
+ if (!directive || typeof directive !== 'string')
54383
+ return 'resource';
54384
+ const d = directive.toLowerCase();
54385
+ if (blockedURI === 'inline') {
54386
+ if (d.includes('script-src-attr')) {
54387
+ return 'inline event handler';
54388
+ }
54389
+ if (d.includes('style-src-attr')) {
54390
+ return 'inline style attribute';
54391
+ }
54392
+ if (d.includes('script')) {
54393
+ return 'inline script';
54394
+ }
54395
+ if (d.includes('style')) {
54396
+ return 'inline style';
54397
+ }
54398
+ }
54399
+ if (blockedURI === 'eval') {
54400
+ return 'eval script execution';
54401
+ }
54402
+ if (d.includes('worker'))
54403
+ return 'worker';
54404
+ if (d.includes('script')) {
54405
+ return d.includes('elem') ? 'script element' : 'script';
54406
+ }
54407
+ if (d.includes('style')) {
54408
+ return d.includes('elem') ? 'style element' : 'stylesheet';
54409
+ }
54410
+ if (d.includes('img'))
54411
+ return 'image';
54412
+ if (d.includes('font'))
54413
+ return 'font';
54414
+ if (d.includes('connect'))
54415
+ return 'network request';
54416
+ if (d.includes('media'))
54417
+ return 'media resource';
54418
+ if (d.includes('frame-ancestors'))
54419
+ return 'display of your page in a frame';
54420
+ if (d.includes('frame'))
54421
+ return 'frame';
54422
+ if (d.includes('manifest'))
54423
+ return 'manifest';
54424
+ if (d.includes('base-uri'))
54425
+ return 'base URI';
54426
+ if (d.includes('form-action'))
54427
+ return 'form submission';
54428
+ return 'resource';
54429
+ }
54430
+ /**
54431
+ * Finds a specific directive in a CSP policy string and returns it with its value.
54432
+ *
54433
+ * @param {string} policy - The complete original CSP policy string (semicolon-separated directives)
54434
+ * @param {string} directiveName - The name of the directive to find (e.g., 'script-src', 'style-src')
54435
+ * @returns {string} The complete directive with its value if found, empty string otherwise
54436
+ *
54437
+ * @example
54438
+ * getDirective('script-src \'self\'; style-src \'self\'', 'script-src') // returns 'script-src \'self\''
54439
+ */
54440
+ function getDirective(policy, directiveName) {
54441
+ if (!policy || !directiveName)
54442
+ return '';
54443
+ const needle = directiveName.toLowerCase();
54444
+ for (const directive of policy.split(';')) {
54445
+ const trimmed = directive.trim();
54446
+ const lower = trimmed.toLowerCase();
54447
+ if (lower === needle || lower.startsWith(`${needle} `)) {
54448
+ return trimmed;
54449
+ }
54450
+ }
54451
+ return '';
54452
+ }
54453
+ /**
54454
+ * Determines the appropriate article (A/An) for a given phrase based on its first letter.
54455
+ *
54456
+ * @param {string} phrase - The phrase to determine the article for
54457
+ * @returns {string} Either 'A' or 'An' based on whether the phrase starts with a vowel sound
54458
+ *
54459
+ * @example
54460
+ * getArticle('script') // returns 'A'
54461
+ * getArticle('image') // returns 'An'
54462
+ * getArticle('stylesheet') // returns 'A'
54463
+ * getArticle('') // returns 'A' (fallback for empty string)
54464
+ * getArticle(undefined) // returns 'A' (fallback for undefined)
54465
+ */
54466
+ function getArticle(phrase) {
54467
+ if (!phrase || typeof phrase !== 'string')
54468
+ return 'A';
54469
+ const c = phrase.trim().charAt(0).toLowerCase();
54470
+ return 'aeiou'.includes(c) ? 'An' : 'A';
54471
+ }
54472
+ /**
54473
+ * Returns the original blocked URI when it looks like a URL or scheme,
54474
+ * otherwise returns an empty string for special cases.
54475
+ *
54476
+ * @param {string} blockedURI - The URI that was blocked (can be 'inline', 'eval', or a URL/scheme)
54477
+ * @returns {string} The blocked URI or an empty string for special cases like 'inline' or 'eval'
54478
+ *
54479
+ * @example
54480
+ * formatBlockedUri('https://example.com/script.js') // returns 'https://example.com/script.js'
54481
+ * formatBlockedUri('inline') // returns ''
54482
+ * formatBlockedUri('blob') // returns 'blob'
54483
+ */
54484
+ function formatBlockedUri(blockedURI) {
54485
+ if (!blockedURI || blockedURI === 'inline' || blockedURI === 'eval')
54486
+ return '';
54487
+ return blockedURI;
54488
+ }
54489
+ /**
54490
+ * Formats a CSP violation message for console output.
54491
+ *
54492
+ * @param {string} blockedURI - The URI that was blocked by the CSP policy (can be 'inline', 'eval', or a URL)
54493
+ * @param {string} directive - The CSP directive that caused the violation (e.g., 'script-src', 'frame-ancestors')
54494
+ * @param {boolean} isReportOnly - Whether this is a report-only policy violation (adds " (Report-Only)" to message)
54495
+ * @param {string} originalPolicy - The complete CSP policy that was violated
54496
+ * @returns {string} A formatted readable CSP violation message
54497
+ *
54498
+ * @example
54499
+ * createCspViolationMessage(
54500
+ * 'https://example.com/script.js',
54501
+ * 'script-src',
54502
+ * false,
54503
+ * 'script-src \'self\'; style-src \'self\''
54504
+ * )
54505
+ * // returns: "Content Security Policy: A script from https://example.com/script.js was blocked by your site's `script-src 'self'` policy.\nCurrent CSP: \"script-src 'self'; style-src 'self'\"."
54506
+ */
54507
+ function createCspViolationMessage(blockedURI, directive, isReportOnly, originalPolicy) {
54508
+ if (!directive || typeof directive !== 'string') {
54509
+ return 'Content Security Policy: Unknown violation occurred.';
54510
+ }
54511
+ try {
54512
+ const reportOnlyText = isReportOnly ? ' (Report-Only)' : '';
54513
+ // special case for frame-ancestors since it doesn't fit our template at all
54514
+ if ((directive === null || directive === void 0 ? void 0 : directive.toLowerCase()) === 'frame-ancestors') {
54515
+ return `Content Security Policy${reportOnlyText}: The display of ${blockedURI} in a frame was blocked because an ancestor violates your site's frame-ancestors policy.\nCurrent CSP: "${originalPolicy}".`;
54516
+ }
54517
+ const resourceType = getResourceType(blockedURI, directive);
54518
+ const article = getArticle(resourceType);
54519
+ const source = formatBlockedUri(blockedURI);
54520
+ const directiveValue = getDirective(originalPolicy, directive);
54521
+ const policyDisplay = directiveValue || directive;
54522
+ const resourceDescription = `${article} ${resourceType}${source ? ` from ${source}` : ''}`;
54523
+ return `Content Security Policy${reportOnlyText}: ${resourceDescription} was blocked by your site's \`${policyDisplay}\` policy.\nCurrent CSP: "${originalPolicy}".`;
54524
+ }
54525
+ catch (error) {
54526
+ return `Content Security Policy: Error formatting violation message: ${error.message}`;
54527
+ }
54528
+ }
54529
+
54161
54530
  const DEV_LOG_LEVELS = ['info', 'warn', 'error'];
54162
54531
  const TOKEN_MAX_SIZE = 100;
54163
54532
  const TOKEN_REFILL_RATE = 10;
@@ -56292,6 +56661,7 @@ function ConsoleCapture() {
56292
56661
  onAppUnloaded,
56293
56662
  onPtmPaused,
56294
56663
  onPtmUnpaused,
56664
+ securityPolicyViolationFn,
56295
56665
  get buffer() {
56296
56666
  return buffer;
56297
56667
  },
@@ -56320,7 +56690,7 @@ function ConsoleCapture() {
56320
56690
  }
56321
56691
  }, SEND_INTERVAL);
56322
56692
  sendQueue.start();
56323
- pluginAPI.Events.ready.on(addIntercepts);
56693
+ pluginAPI.Events.ready.on(readyHandler);
56324
56694
  pluginAPI.Events.appUnloaded.on(onAppUnloaded);
56325
56695
  pluginAPI.Events.appHidden.on(onAppHidden);
56326
56696
  pluginAPI.Events['ptm:paused'].on(onPtmPaused);
@@ -56355,6 +56725,10 @@ function ConsoleCapture() {
56355
56725
  url: globalPendo.url.get()
56356
56726
  };
56357
56727
  }
56728
+ function readyHandler() {
56729
+ addIntercepts();
56730
+ pluginAPI.attachEventInternal(window, 'securitypolicyviolation', securityPolicyViolationFn);
56731
+ }
56358
56732
  function addIntercepts() {
56359
56733
  _.each(CONSOLE_METHODS, function (methodName) {
56360
56734
  const originalMethod = console[methodName];
@@ -56371,7 +56745,21 @@ function ConsoleCapture() {
56371
56745
  };
56372
56746
  });
56373
56747
  }
56374
- function createConsoleEvent(args, methodName) {
56748
+ function securityPolicyViolationFn(evt) {
56749
+ if (!evt || typeof evt !== 'object')
56750
+ return;
56751
+ const { blockedURI, violatedDirective, effectiveDirective, disposition, originalPolicy } = evt;
56752
+ const directive = violatedDirective || effectiveDirective;
56753
+ const isReportOnly = disposition === 'report';
56754
+ const message = createCspViolationMessage(blockedURI, directive, isReportOnly, originalPolicy);
56755
+ if (isReportOnly) {
56756
+ createConsoleEvent([message], 'warn', { skipStackTrace: true, skipScrubPII: true });
56757
+ }
56758
+ else {
56759
+ createConsoleEvent([message], 'error', { skipStackTrace: true, skipScrubPII: true });
56760
+ }
56761
+ }
56762
+ function createConsoleEvent(args, methodName, { skipStackTrace = false, skipScrubPII = false } = {}) {
56375
56763
  if (!args || args.length === 0)
56376
56764
  return;
56377
56765
  // stringify args
@@ -56388,8 +56776,11 @@ function ConsoleCapture() {
56388
56776
  if (!message)
56389
56777
  return;
56390
56778
  // capture stack trace
56391
- const maxStackFrames = methodName === 'error' ? 4 : 1;
56392
- let stackTrace = captureStackTrace(maxStackFrames);
56779
+ let stackTrace = '';
56780
+ if (!skipStackTrace) {
56781
+ const maxStackFrames = methodName === 'error' ? 4 : 1;
56782
+ stackTrace = captureStackTrace(maxStackFrames);
56783
+ }
56393
56784
  // truncate message and stack trace
56394
56785
  message = truncate(message);
56395
56786
  stackTrace = truncate(stackTrace);
@@ -56400,7 +56791,7 @@ function ConsoleCapture() {
56400
56791
  return;
56401
56792
  }
56402
56793
  const devLogEnvelope = createDevLogEnvelope();
56403
- const consoleEvent = Object.assign(Object.assign({}, devLogEnvelope), { devLogLevel: methodName === 'log' ? 'info' : methodName, devLogMessage: scrubPII(message), devLogTrace: stackTrace, devLogCount: 1 });
56794
+ const consoleEvent = Object.assign(Object.assign({}, devLogEnvelope), { devLogLevel: methodName === 'log' ? 'info' : methodName, devLogMessage: skipScrubPII ? message : scrubPII(message), devLogTrace: stackTrace, devLogCount: 1 });
56404
56795
  const wasAccepted = buffer.push(consoleEvent);
56405
56796
  if (wasAccepted) {
56406
56797
  if (!isPtmPaused) {
@@ -56462,6 +56853,7 @@ function ConsoleCapture() {
56462
56853
  pluginAPI.Events.appUnloaded.off(onAppUnloaded);
56463
56854
  pluginAPI.Events['ptm:paused'].off(onPtmPaused);
56464
56855
  pluginAPI.Events['ptm:unpaused'].off(onPtmUnpaused);
56856
+ pluginAPI.detachEventInternal(window, 'securitypolicyviolation', securityPolicyViolationFn);
56465
56857
  _.each(CONSOLE_METHODS, function (methodName) {
56466
56858
  if (!console[methodName])
56467
56859
  return _.noop;