@pendo/agent 2.293.1 → 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.
@@ -3908,8 +3908,8 @@ var SERVER = '';
3908
3908
  var ASSET_HOST = '';
3909
3909
  var ASSET_PATH = '';
3910
3910
  var DESIGNER_SERVER = '';
3911
- var VERSION = '2.293.1_';
3912
- var PACKAGE_VERSION = '2.293.1';
3911
+ var VERSION = '2.294.0_';
3912
+ var PACKAGE_VERSION = '2.294.0';
3913
3913
  var LOADER = 'xhr';
3914
3914
  /* eslint-enable agent-eslint-rules/no-gulp-env-references */
3915
3915
  /**
@@ -12231,6 +12231,7 @@ class PerformanceMonitor {
12231
12231
  return detectNativeBrowserAPI('performance.mark') &&
12232
12232
  detectNativeBrowserAPI('performance.measure') &&
12233
12233
  detectNativeBrowserAPI('performance.getEntries') &&
12234
+ detectNativeBrowserAPI('performance.getEntriesByName') &&
12234
12235
  detectNativeBrowserAPI('performance.clearMarks') &&
12235
12236
  detectNativeBrowserAPI('performance.clearMeasures');
12236
12237
  }
@@ -12250,8 +12251,10 @@ class PerformanceMonitor {
12250
12251
  name = `pendo-${name}`;
12251
12252
  const startMark = `${name}-start`;
12252
12253
  const stopMark = `${name}-stop`;
12253
- performance.measure(name, startMark, stopMark);
12254
- 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
+ }
12255
12258
  }
12256
12259
  _count(registry, name) {
12257
12260
  if (!registry[name]) {
@@ -13304,6 +13307,7 @@ var getValidTarget = function (node) {
13304
13307
  */
13305
13308
  var handle_event = function (evt) {
13306
13309
  try {
13310
+ PerformanceMonitor$1.startTimer('event-captured');
13307
13311
  if (dom.data.get(evt, 'counted'))
13308
13312
  return;
13309
13313
  dom.data.set(evt, 'counted', true);
@@ -13343,6 +13347,9 @@ var handle_event = function (evt) {
13343
13347
  catch (e) {
13344
13348
  log.critical('pendo.io while handling event', { error: e });
13345
13349
  }
13350
+ finally {
13351
+ PerformanceMonitor$1.stopTimer('event-captured');
13352
+ }
13346
13353
  };
13347
13354
  function getClickEventProperties(target) {
13348
13355
  const eventPropertyHandler = getEventPropertyHandler(target);
@@ -17866,6 +17873,9 @@ function getAllowedAttributes(attributeKeyValueMap, stepId, guideId, type) {
17866
17873
  function buildNodeFromJSON(json, step, guides) {
17867
17874
  step = step || { id: 'unknown', guideId: 'unknown' };
17868
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
+ }
17869
17879
  var curNode = dom(document.createElement(json.type));
17870
17880
  // APP-81040 calling code in the pendo app (and possibly elsewhere) depends on
17871
17881
  // curNode.getParent() returning non-null in some cases. The fact that it used to
@@ -17885,7 +17895,11 @@ function buildNodeFromJSON(json, step, guides) {
17885
17895
  }
17886
17896
  _.each(json.props, function (propValue, propKey) {
17887
17897
  if (propKey === 'style') {
17888
- 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);
17889
17903
  }
17890
17904
  else if (propKey === 'data-pendo-code-block' && propValue === true && !ConfigReader.get('preventCodeInjection')) {
17891
17905
  const htmlString = step.getContent();
@@ -17910,13 +17924,23 @@ function buildNodeFromJSON(json, step, guides) {
17910
17924
  if (nonce) {
17911
17925
  curNode.attr('nonce', nonce);
17912
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
+ }
17913
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.
17914
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.
17915
17939
  if (curNode.styleSheet) {
17916
- curNode.styleSheet.cssText = buildStyleTagContent(json.css);
17940
+ curNode.styleSheet.cssText = buildStyleTagContent(css);
17917
17941
  }
17918
17942
  else {
17919
- curNode.text(buildStyleTagContent(json.css));
17943
+ curNode.text(buildStyleTagContent(css));
17920
17944
  }
17921
17945
  }
17922
17946
  if (json.svgWidgetId) {
@@ -20614,6 +20638,21 @@ class AutoDisplayPhase {
20614
20638
  }
20615
20639
  }
20616
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
+
20617
20656
  /*
20618
20657
  * Guide Loop
20619
20658
  *
@@ -20812,6 +20851,7 @@ function stepShowingProc(guide, step) {
20812
20851
  step.attributes.currentTextZoomFontSize = currentBrowserFontSize;
20813
20852
  }
20814
20853
  }
20854
+ syncColorMode(step, true);
20815
20855
  if (step.elementPathRule && targetElement && !SizzleProxy.matchesSelector(targetElement, step.elementPathRule)) {
20816
20856
  step.hide();
20817
20857
  return;
@@ -26340,53 +26380,152 @@ class NetworkRequestIntercept {
26340
26380
  extractXHRHeaders(xhr) {
26341
26381
  return xhr[PENDO_HEADERS_KEY] || {};
26342
26382
  }
26343
- 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() {
26344
26429
  const networkInterceptor = this;
26345
- if (networkInterceptor._networkPatched)
26346
- return;
26347
- networkInterceptor._networkPatched = true;
26348
- networkInterceptor._originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
26349
- XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
26350
- this[PENDO_HEADERS_KEY] = this[PENDO_HEADERS_KEY] || {};
26351
- this[PENDO_HEADERS_KEY][header] = value;
26352
- return networkInterceptor._originalSetRequestHeader.apply(this, arguments);
26353
- };
26354
26430
  networkInterceptor._originalFetch = window.fetch;
26355
26431
  window.fetch = function (...args) {
26356
26432
  return __awaiter(this, void 0, void 0, function* () {
26357
26433
  const [url, config = {}] = args;
26358
26434
  const method = config.method || 'GET';
26435
+ const requestId = networkInterceptor.generateRequestId();
26436
+ // Capture request data
26359
26437
  try {
26360
26438
  const headers = networkInterceptor.extractHeaders(config.headers);
26361
26439
  const body = networkInterceptor.safelySerializeBody(config.body);
26362
26440
  _.each(networkInterceptor.callbackFns, ({ request }) => {
26363
26441
  if (_.isFunction(request))
26364
- request({ method, url, body, headers });
26442
+ request({ requestId, method, url, body, headers });
26365
26443
  });
26366
26444
  }
26367
26445
  catch (e) {
26368
26446
  _.each(networkInterceptor.callbackFns, ({ error }) => {
26369
26447
  if (_.isFunction(error))
26370
- 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
+ }
26371
26498
  });
26499
+ throw error;
26372
26500
  }
26373
- return networkInterceptor._originalFetch.apply(this, args);
26374
26501
  });
26375
26502
  };
26503
+ }
26504
+ patchXHR() {
26505
+ const networkInterceptor = this;
26506
+ networkInterceptor._originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
26376
26507
  networkInterceptor._originalXHROpen = XMLHttpRequest.prototype.open;
26377
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
+ };
26378
26514
  XMLHttpRequest.prototype.open = function (method, url) {
26379
- this._method = method; // Store data per XHR instance
26515
+ this._method = method;
26380
26516
  this._url = url;
26517
+ this._requestId = networkInterceptor.generateRequestId();
26381
26518
  return networkInterceptor._originalXHROpen.apply(this, arguments);
26382
26519
  };
26383
26520
  XMLHttpRequest.prototype.send = function (body) {
26521
+ const xhr = this;
26522
+ const headers = networkInterceptor.extractXHRHeaders(this);
26523
+ const safeBody = networkInterceptor.safelySerializeBody(body);
26384
26524
  try {
26385
- const headers = networkInterceptor.extractXHRHeaders(this);
26386
- const safeBody = networkInterceptor.safelySerializeBody(body);
26387
26525
  _.each(networkInterceptor.callbackFns, ({ request }) => {
26388
26526
  if (_.isFunction(request)) {
26389
26527
  request({
26528
+ requestId: this._requestId,
26390
26529
  method: this._method || 'GET',
26391
26530
  url: this._url,
26392
26531
  body: safeBody,
@@ -26398,7 +26537,7 @@ class NetworkRequestIntercept {
26398
26537
  catch (e) {
26399
26538
  _.each(networkInterceptor.callbackFns, ({ error }) => {
26400
26539
  if (_.isFunction(error))
26401
- error({ context: 'xhr', error: e });
26540
+ error({ context: 'xhrRequest', error: e });
26402
26541
  });
26403
26542
  }
26404
26543
  // Clean up custom headers after the request completes
@@ -26407,9 +26546,49 @@ class NetworkRequestIntercept {
26407
26546
  delete this[PENDO_HEADERS_KEY];
26408
26547
  }, { once: true });
26409
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
+ };
26410
26581
  return networkInterceptor._originalXHRSend.apply(this, arguments);
26411
26582
  };
26412
26583
  }
26584
+ patchNetwork() {
26585
+ const networkInterceptor = this;
26586
+ if (networkInterceptor._networkPatched)
26587
+ return;
26588
+ networkInterceptor._networkPatched = true;
26589
+ networkInterceptor.patchFetch();
26590
+ networkInterceptor.patchXHR();
26591
+ }
26413
26592
  on({ request, response, error } = {}) {
26414
26593
  if (!this.callbackFns) {
26415
26594
  this.callbackFns = [];
@@ -26417,7 +26596,7 @@ class NetworkRequestIntercept {
26417
26596
  if (_.isFunction(request) || _.isFunction(response) || _.isFunction(error)) {
26418
26597
  this.callbackFns.push({ request, response, error });
26419
26598
  }
26420
- if (!this._networkPatched && !this._patching) {
26599
+ if (!this._networkPatched) {
26421
26600
  this.patchNetwork();
26422
26601
  }
26423
26602
  return this;
@@ -27237,6 +27416,8 @@ const PluginAPI = {
27237
27416
  collectEvent
27238
27417
  },
27239
27418
  attachEvent,
27419
+ attachEventInternal,
27420
+ detachEventInternal,
27240
27421
  ConfigReader,
27241
27422
  Events,
27242
27423
  EventTracer,
@@ -28201,6 +28382,9 @@ var BuildingBlockGuides = (function () {
28201
28382
  function renderGuideFromJSON(json, step, guides, options) {
28202
28383
  options = options || {};
28203
28384
  var guide = step.getGuide();
28385
+ if (step.isDarkMode === undefined) {
28386
+ syncColorMode(step);
28387
+ }
28204
28388
  var containerJSON = findGuideContainerJSON(json);
28205
28389
  var isResourceCenter = _.get(guide, 'attributes.resourceCenter');
28206
28390
  if (isResourceCenter) {
@@ -31068,6 +31252,10 @@ var GuideDisplay = (function () {
31068
31252
  if (step.type !== 'whatsnew' && !step.isShown()) {
31069
31253
  return false;
31070
31254
  }
31255
+ if (isGuideRequestPending()) {
31256
+ log.info('guides are loading.', { contexts: ['guides', 'loading'] });
31257
+ return false;
31258
+ }
31071
31259
  step.unlock();
31072
31260
  step._show(reason);
31073
31261
  return step.isShown();
@@ -31552,6 +31740,7 @@ function GuideStep(guide) {
31552
31740
  step.seenState = 'active';
31553
31741
  setSeenTime(getNow());
31554
31742
  var seenProps = {
31743
+ color_mode: step.isDarkMode ? 'dark' : 'default',
31555
31744
  last_updated_at: step.lastUpdatedAt
31556
31745
  };
31557
31746
  var pollTypes = this.getStepPollTypes(guide, step);
@@ -33243,7 +33432,7 @@ const DebuggerModule = (() => {
33243
33432
  location: { type: SYNC_TYPES.BOTTOM_UP, defaultValue: {} }
33244
33433
  };
33245
33434
  const actions = {
33246
- init: (context) => {
33435
+ init: (context, buffer) => {
33247
33436
  context.commit('setFrameId', EventTracer.getFrameId());
33248
33437
  context.commit('setTabId', EventTracer.getTabId());
33249
33438
  context.commit('setInstallType', getInstallType());
@@ -33263,6 +33452,7 @@ const DebuggerModule = (() => {
33263
33452
  frameId: context.state.frameId
33264
33453
  });
33265
33454
  }
33455
+ _.each(buffer.events, (event) => eventCapturedFn(event));
33266
33456
  },
33267
33457
  join: (context, data) => {
33268
33458
  if (context.state.frameId === data.frameId)
@@ -36956,12 +37146,18 @@ function disableDebugging() {
36956
37146
  _.extend(debug, debugging);
36957
37147
 
36958
37148
  const waitForLeader = (Events, store, q) => {
37149
+ const buffer = {
37150
+ events: []
37151
+ };
37152
+ Events.on('eventCaptured', (evt) => {
37153
+ buffer.events.push(evt);
37154
+ });
36959
37155
  if (store.getters['frames/leaderExists']()) {
36960
- return q.resolve();
37156
+ return q.resolve(buffer);
36961
37157
  }
36962
37158
  const deferred = q.defer();
36963
37159
  Events.on('leaderChanged', () => {
36964
- deferred.resolve();
37160
+ deferred.resolve(buffer);
36965
37161
  });
36966
37162
  return deferred.promise;
36967
37163
  };
@@ -36976,8 +37172,8 @@ const DebuggerLauncher = (function () {
36976
37172
  function initialize(pendo, PluginAPI) {
36977
37173
  const { Events, q } = PluginAPI;
36978
37174
  store = PluginAPI.store;
36979
- waitForLeader(Events, store, q).then(() => {
36980
- store.dispatch('debugger/init');
37175
+ waitForLeader(Events, store, q).then((buffer) => {
37176
+ store.dispatch('debugger/init', buffer);
36981
37177
  if (store.getters['frames/isLeader']()) {
36982
37178
  startDebuggingModuleIfEnabled();
36983
37179
  }
@@ -44343,13 +44539,15 @@ class MutationBuffer {
44343
44539
  index.childNodes(textarea),
44344
44540
  (cn) => index.textContent(cn) || ""
44345
44541
  ).join("");
44542
+ const needsMask = needMaskingText(textarea, this.maskTextClass, this.maskTextSelector, true);
44346
44543
  item.attributes.value = maskInputValue({
44347
44544
  element: textarea,
44348
44545
  maskInputOptions: this.maskInputOptions,
44349
44546
  tagName: textarea.tagName,
44350
44547
  type: getInputType(textarea),
44351
44548
  value,
44352
- maskInputFn: this.maskInputFn
44549
+ maskInputFn: this.maskInputFn,
44550
+ needsMask
44353
44551
  });
44354
44552
  });
44355
44553
  __publicField(this, "processMutation", (m) => {
@@ -52555,6 +52753,12 @@ class SessionRecorder {
52555
52753
  return;
52556
52754
  clearTimeout(this._changeIdentityTimer);
52557
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
+ });
52558
52762
  this.stop();
52559
52763
  }
52560
52764
  this.visitorId = identifyEvent.data[0].visitor_id;
@@ -52650,7 +52854,7 @@ class SessionRecorder {
52650
52854
  this._start();
52651
52855
  }
52652
52856
  _markEvents(events) {
52653
- if (!this.recordingId)
52857
+ if (!this.recordingId || !this.isRecording())
52654
52858
  return;
52655
52859
  for (var e of events) {
52656
52860
  if ((e.visitor_id === this.visitorId || e.visitorId === this.visitorId) && e.type !== 'identify') {
@@ -52968,6 +53172,8 @@ class SessionRecorder {
52968
53172
  }
52969
53173
  }
52970
53174
  addRecordingId(event) {
53175
+ if (!this.isRecording())
53176
+ return;
52971
53177
  if (!this.recordingId || !event || !event.data || !event.data.length)
52972
53178
  return;
52973
53179
  var capturedEvent = event.data[0];
@@ -52976,6 +53182,10 @@ class SessionRecorder {
52976
53182
  if (capturedEvent.visitor_id !== this.visitorId && capturedEvent.visitorId !== this.visitorId) {
52977
53183
  // visitor id has already diverged from the agent, we'll stop sending events and just return here
52978
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
+ });
52979
53189
  this.stop();
52980
53190
  return;
52981
53191
  }
@@ -54155,6 +54365,168 @@ function scrubPII(string) {
54155
54365
  return Object.values(PII_PATTERN).reduce((str, pattern) => str.replace(pattern, PII_REPLACEMENT), string);
54156
54366
  }
54157
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
+
54158
54530
  const DEV_LOG_LEVELS = ['info', 'warn', 'error'];
54159
54531
  const TOKEN_MAX_SIZE = 100;
54160
54532
  const TOKEN_REFILL_RATE = 10;
@@ -56289,6 +56661,7 @@ function ConsoleCapture() {
56289
56661
  onAppUnloaded,
56290
56662
  onPtmPaused,
56291
56663
  onPtmUnpaused,
56664
+ securityPolicyViolationFn,
56292
56665
  get buffer() {
56293
56666
  return buffer;
56294
56667
  },
@@ -56317,7 +56690,7 @@ function ConsoleCapture() {
56317
56690
  }
56318
56691
  }, SEND_INTERVAL);
56319
56692
  sendQueue.start();
56320
- pluginAPI.Events.ready.on(addIntercepts);
56693
+ pluginAPI.Events.ready.on(readyHandler);
56321
56694
  pluginAPI.Events.appUnloaded.on(onAppUnloaded);
56322
56695
  pluginAPI.Events.appHidden.on(onAppHidden);
56323
56696
  pluginAPI.Events['ptm:paused'].on(onPtmPaused);
@@ -56352,6 +56725,10 @@ function ConsoleCapture() {
56352
56725
  url: globalPendo.url.get()
56353
56726
  };
56354
56727
  }
56728
+ function readyHandler() {
56729
+ addIntercepts();
56730
+ pluginAPI.attachEventInternal(window, 'securitypolicyviolation', securityPolicyViolationFn);
56731
+ }
56355
56732
  function addIntercepts() {
56356
56733
  _.each(CONSOLE_METHODS, function (methodName) {
56357
56734
  const originalMethod = console[methodName];
@@ -56368,7 +56745,21 @@ function ConsoleCapture() {
56368
56745
  };
56369
56746
  });
56370
56747
  }
56371
- 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 } = {}) {
56372
56763
  if (!args || args.length === 0)
56373
56764
  return;
56374
56765
  // stringify args
@@ -56385,8 +56776,11 @@ function ConsoleCapture() {
56385
56776
  if (!message)
56386
56777
  return;
56387
56778
  // capture stack trace
56388
- const maxStackFrames = methodName === 'error' ? 4 : 1;
56389
- let stackTrace = captureStackTrace(maxStackFrames);
56779
+ let stackTrace = '';
56780
+ if (!skipStackTrace) {
56781
+ const maxStackFrames = methodName === 'error' ? 4 : 1;
56782
+ stackTrace = captureStackTrace(maxStackFrames);
56783
+ }
56390
56784
  // truncate message and stack trace
56391
56785
  message = truncate(message);
56392
56786
  stackTrace = truncate(stackTrace);
@@ -56397,7 +56791,7 @@ function ConsoleCapture() {
56397
56791
  return;
56398
56792
  }
56399
56793
  const devLogEnvelope = createDevLogEnvelope();
56400
- 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 });
56401
56795
  const wasAccepted = buffer.push(consoleEvent);
56402
56796
  if (wasAccepted) {
56403
56797
  if (!isPtmPaused) {
@@ -56459,6 +56853,7 @@ function ConsoleCapture() {
56459
56853
  pluginAPI.Events.appUnloaded.off(onAppUnloaded);
56460
56854
  pluginAPI.Events['ptm:paused'].off(onPtmPaused);
56461
56855
  pluginAPI.Events['ptm:unpaused'].off(onPtmUnpaused);
56856
+ pluginAPI.detachEventInternal(window, 'securitypolicyviolation', securityPolicyViolationFn);
56462
56857
  _.each(CONSOLE_METHODS, function (methodName) {
56463
56858
  if (!console[methodName])
56464
56859
  return _.noop;