@pendo/agent 2.337.3 → 2.338.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.
@@ -430,6 +430,12 @@ function getPolicy(pendo) {
430
430
  return policy;
431
431
  }
432
432
 
433
+ function escapeRegExp(string) {
434
+ if (!string)
435
+ return;
436
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
437
+ }
438
+
433
439
  var STAGING_SERVER_HASHES = 'stagingServerHashes';
434
440
  var STAGING_DOMAINS = 'stagingDomains';
435
441
  var pendo$1;
@@ -515,7 +521,9 @@ function urlMatchesPatterns(url, patterns = [], hashes = []) {
515
521
  }
516
522
  }
517
523
  if (typeof pattern === 'string') {
518
- pattern = new RegExp('^' + pattern + '$');
524
+ // String entries are literal origins/hosts, not patterns. Escape
525
+ // them so a '.' can't act as a wildcard and match a look-alike domain.
526
+ pattern = new RegExp('^' + escapeRegExp(pattern) + '$');
519
527
  }
520
528
  if (pattern instanceof RegExp && pattern.test(url)) {
521
529
  return true;
@@ -3916,7 +3924,13 @@ function isMobileUserAgent() {
3916
3924
  return (/Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i).test(getUA());
3917
3925
  }
3918
3926
  function isSafari(userAgent = getUA()) {
3919
- return userAgent.indexOf('Safari') >= 0 && !userAgent.indexOf('Chrome') >= 0;
3927
+ return userAgent.indexOf('Safari') >= 0 && !isChromium(userAgent);
3928
+ }
3929
+ // Every Chromium UA carries a trailing Safari/ token, so Chrome, Edge, Opera,
3930
+ // Brave and Electron all have to be excluded from isSafari(). Chromium builds
3931
+ // report Chromium/ without a Chrome/ token, hence both checks.
3932
+ function isChromium(userAgent = getUA()) {
3933
+ return userAgent.indexOf('Chrome') >= 0 || userAgent.indexOf('Chromium') >= 0;
3920
3934
  }
3921
3935
  const sniffer = {
3922
3936
  supportsHistoryApi: shouldWrapNativeHistory,
@@ -4029,8 +4043,8 @@ let SERVER = '';
4029
4043
  let ASSET_HOST = '';
4030
4044
  let ASSET_PATH = '';
4031
4045
  let DESIGNER_SERVER = '';
4032
- let VERSION = '2.337.3_';
4033
- let PACKAGE_VERSION = '2.337.3';
4046
+ let VERSION = '2.338.0_';
4047
+ let PACKAGE_VERSION = '2.338.0';
4034
4048
  let LOADER = 'xhr';
4035
4049
  /* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
4036
4050
  /**
@@ -4447,12 +4461,6 @@ class LogEmitter extends EventTarget {
4447
4461
  }
4448
4462
  const log = new LogEmitter();
4449
4463
 
4450
- function escapeRegExp(string) {
4451
- if (!string)
4452
- return;
4453
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4454
- }
4455
-
4456
4464
  const allowKeys = function () {
4457
4465
  var allowKeys = ConfigReader.get('storage.allowKeys') || '*';
4458
4466
  return _.isArray(allowKeys) ? _.indexBy(allowKeys) : allowKeys;
@@ -10079,6 +10087,26 @@ var writeMessage = function (msg) {
10079
10087
  });
10080
10088
  return q.resolve(writeImgTag(url)).catch(_.noop);
10081
10089
  };
10090
+ /**
10091
+ * Releases a fetch response body we are never going to read. An undrained body
10092
+ * holds its stream, the underlying data pipe and the socket open until the
10093
+ * {Response} is collected, which may never happen in a long-lived embedded
10094
+ * renderer. Cancelling does not affect keepalive delivery.
10095
+ *
10096
+ * @access private
10097
+ * @param {Response} response to discard the body of
10098
+ * @returns {Response} the same response, for chaining
10099
+ */
10100
+ function cancelResponseBody(response) {
10101
+ try {
10102
+ if (response && response.body && _.isFunction(response.body.cancel)) {
10103
+ response.body.cancel().catch(_.noop);
10104
+ }
10105
+ }
10106
+ catch (e) {
10107
+ }
10108
+ return response;
10109
+ }
10082
10110
  /**
10083
10111
  * @access private
10084
10112
  * @param {string} msg to send
@@ -10107,7 +10135,9 @@ function writeErrorPOST(msg) {
10107
10135
  else {
10108
10136
  promise = ajax.postJSON(url, errorData);
10109
10137
  }
10110
- return promise.then(function () { log.info('successfully wrote error'); }, function (err) { log.info('error writing error:' + err); });
10138
+ return promise.then(function (response) {
10139
+ cancelResponseBody(response);
10140
+ }, function (err) { log.debug('error writing error:' + err); });
10111
10141
  }
10112
10142
  catch (e) {
10113
10143
  log.info('Failed to write error to server using POST endpoint: ' + e);
@@ -10129,6 +10159,10 @@ function writeMetricsPOST(payload) {
10129
10159
  keepalive: true,
10130
10160
  body: JSON.stringify(payload),
10131
10161
  headers: { 'Content-Type': 'application/json' }
10162
+ })
10163
+ .then(cancelResponseBody)
10164
+ .catch(function (err) {
10165
+ log.debug(`Failed to write metrics payload to server: ${err}`);
10132
10166
  });
10133
10167
  }
10134
10168
  else {
@@ -10156,13 +10190,23 @@ function writeImgTag(src) {
10156
10190
  image.src = src;
10157
10191
  return deferred.promise;
10158
10192
  }
10193
+ /**
10194
+ * Sends a fire-and-forget beacon request. The response body is cancelled before
10195
+ * the promise resolves, so callers get {Response.status}, {Response.ok} and the
10196
+ * headers but cannot read the body. Use {fetch} directly if you need to read one.
10197
+ *
10198
+ * @access private
10199
+ * @param {string} url to request
10200
+ * @param {Object} options passed to {fetch}, defaulting to a keepalive GET
10201
+ * @returns {Promise<Response>} with its body already cancelled
10202
+ */
10159
10203
  function fetchKeepalive(url, options = {}) {
10160
10204
  const defaults = {
10161
10205
  method: 'GET',
10162
10206
  keepalive: true
10163
10207
  };
10164
10208
  const fetchOptions = _.extend({}, defaults, options);
10165
- return fetch(url, fetchOptions);
10209
+ return fetch(url, fetchOptions).then(cancelResponseBody);
10166
10210
  }
10167
10211
  fetchKeepalive.supported = function () {
10168
10212
  try {
@@ -11788,6 +11832,13 @@ _.extend(dom, {
11788
11832
  });
11789
11833
  _.extend(dom.prototype, DomEvent.$, DomQuery.$);
11790
11834
 
11835
+ // Domains that Pendo owns, will be swapped in by build patches
11836
+ const pendoDomainPatterns = [
11837
+ /^https:\/\/((adopt|oem)\.)?((us1)\.)?(app|via|adopt|cdn|oem|novus-api|novus)(\.(au|eu|gov|hsbc|jpn|tv))?\.pendo\.io$/,
11838
+ /^https:\/\/([0-9]{8}t[0-9]{4}-dot-)(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-tv|pendo-us1)\.appspot\.com$/,
11839
+ /^https:\/\/hotfix-(ops|app)-([0-9]+-dot-)(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-tv|pendo-us1)\.appspot\.com$/,
11840
+ /^https:\/\/(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-tv|pendo-us1)-static\.storage\.googleapis\.com$/
11841
+ ]; // eslint-disable-line web-sdk-eslint-rules/no-gulp-env-references
11791
11842
  function isTrustedOrigin(host) {
11792
11843
  if (!host)
11793
11844
  return false;
@@ -11801,13 +11852,6 @@ function isTrustedOrigin(host) {
11801
11852
  return true;
11802
11853
  if (host === getContentHost())
11803
11854
  return true;
11804
- // Domains that Pendo owns, will be swapped in by build patches
11805
- const patterns = [
11806
- /^https:\/\/((adopt|oem)\.)?((us1)\.)?(app|via|adopt|cdn|oem|novus-api|novus)(\.(au|eu|gov|hsbc|jpn|tv))?\.pendo\.io$/,
11807
- /^https:\/\/([0-9]{8}t[0-9]{4}-dot-)(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-tv|pendo-us1)\.appspot\.com$/,
11808
- /^https:\/\/hotfix-(ops|app)-([0-9]+-dot-)(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-tv|pendo-us1)\.appspot\.com$/,
11809
- /^https:\/\/(pendo-au|pendo-eu|pendo-govramp|pendo-hsbc|pendo-io|pendo-jp-prod|pendo-tv|pendo-us1)-static\.storage\.googleapis\.com$/
11810
- ]; // eslint-disable-line web-sdk-eslint-rules/no-gulp-env-references
11811
11855
  var adoptHost = ConfigReader.get('adoptHost');
11812
11856
  if (adoptHost) {
11813
11857
  var fullHostname = 'https://' + adoptHost;
@@ -11817,10 +11861,17 @@ function isTrustedOrigin(host) {
11817
11861
  }
11818
11862
  if (urlMatchesPatterns(host, ConfigReader.get('allowedOriginServers', []), ConfigReader.get('allowedOriginServerHashes', [])))
11819
11863
  return true;
11820
- return _.any(patterns, function (pattern) {
11864
+ return _.any(pendoDomainPatterns, function (pattern) {
11821
11865
  return pattern.test(host);
11822
11866
  });
11823
11867
  }
11868
+ function isPendoDomain(origin) {
11869
+ if (!origin)
11870
+ return false;
11871
+ return _.any(pendoDomainPatterns, function (pattern) {
11872
+ return pattern.test(origin);
11873
+ });
11874
+ }
11824
11875
  function hasAllowedOriginServers() {
11825
11876
  const allowedOriginServers = ConfigReader.get('allowedOriginServers', []);
11826
11877
  return allowedOriginServers && allowedOriginServers.length > 0;
@@ -13158,6 +13209,7 @@ function sendWithImg(apiKey, silo) {
13158
13209
  return writeImgTag(buildGetRequestUrl(apiKey, silo));
13159
13210
  }
13160
13211
  function handleFetchResponse(response) {
13212
+ cancelResponseBody(response);
13161
13213
  if (response.status < 200 || response.status >= 300) {
13162
13214
  return q.reject(new Error(`received status code ${response.status}: ${response.statusText}`));
13163
13215
  }
@@ -26801,10 +26853,23 @@ var DesignerV2 = (function () {
26801
26853
  }
26802
26854
  return validTarget;
26803
26855
  }
26856
+ function hasSameOriginOpener() {
26857
+ if (!window.opener)
26858
+ return false;
26859
+ try {
26860
+ return window.opener.location.origin === window.location.origin;
26861
+ }
26862
+ catch (e) {
26863
+ return false;
26864
+ }
26865
+ }
26804
26866
  function launchOnToken(url) {
26805
26867
  if (isDesignerFrame(window.name)) {
26806
26868
  return;
26807
26869
  }
26870
+ if (isPendoDomain(window.location.origin) && !hasSameOriginOpener()) {
26871
+ return;
26872
+ }
26808
26873
  var tokenParamTest = /pendo-designer=([A-Za-z0-9-]+)/;
26809
26874
  if (!tokenParamTest.test(url))
26810
26875
  return;
@@ -26817,7 +26882,7 @@ var DesignerV2 = (function () {
26817
26882
  // clear stored designer state if we're launching from a token
26818
26883
  window.localStorage.removeItem('pendo-navigation-state');
26819
26884
  window.localStorage.removeItem('pendo-designer-mode');
26820
- launchInAppDesigner({
26885
+ DesignerV2.launchInAppDesigner({
26821
26886
  target: validTarget,
26822
26887
  host: validAssetHost,
26823
26888
  preloader: false,
@@ -29734,7 +29799,11 @@ var BuildingBlockGuides = (function () {
29734
29799
  // position and calculate its height before making it visible.
29735
29800
  // In the designer, changing visibility like this causes the guide to "flicker"
29736
29801
  // between re-renders
29737
- guideContainer.css({ visibility: 'hidden' });
29802
+ // Only hide the first match. Resource center module previews nest the module's
29803
+ // pendo-g- inside the home view's pendo-g- under the same id (via
29804
+ // correctGuideContainerId), and imageNodeHandler only un-hides the first match,
29805
+ // so hiding every match left the module content hidden forever.
29806
+ dom(guideContainer[0]).css({ visibility: 'hidden' });
29738
29807
  guideToAppend.css({ visibility: 'hidden' });
29739
29808
  const embedConfig = _.get(guide, 'attributes.embedConfig', null);
29740
29809
  var watermark = BuildingBlockWatermark.buildWatermark({
@@ -50694,6 +50763,7 @@ const RECORDING_CONFIG_ON_RECORDING_STOP = 'recording.onRecordingStop';
50694
50763
  const RECORDING_CONFIG_WORKER_OVERRIDE = 'recording.workerOverride';
50695
50764
  const RECORDING_CONFIG_TREAT_IFRAME_AS_ROOT = 'recording.treatIframeAsRoot';
50696
50765
  const RECORDING_CONFIG_DISABLE_UNLOAD = 'recording.disableUnload';
50766
+ const RECORDING_CONFIG_RECORD_CROSS_ORIGIN_IFRAMES = 'recording.recordCrossOriginIframes';
50697
50767
  const RESOURCE_CACHING = 'resourceCaching';
50698
50768
  const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
50699
50769
  const ONE_MINUTE_IN_MILLISECONDS = 60 * 1000;
@@ -50805,6 +50875,26 @@ class SessionRecorder {
50805
50875
  * @type {boolean}
50806
50876
  */
50807
50877
  cf.addOption(RECORDING_CONFIG_TREAT_IFRAME_AS_ROOT, [cf.sources.SNIPPET_SRC], undefined);
50878
+ /**
50879
+ * Controls whether content embedded from another domain is included in replay. When enabled, the
50880
+ * embedded content shows up as part of the page. When disabled, it appears as a blank box.
50881
+ *
50882
+ * This only applies to embedded content that also has Pendo installed. Embeds from other companies
50883
+ * (Stripe, YouTube) are never captured either way — turning this off just stops Pendo from
50884
+ * messaging those frames at all.
50885
+ *
50886
+ * To capture embedded content as its own separate replay instead of part of the page it sits in,
50887
+ * see recording.treatIframeAsRoot.
50888
+ *
50889
+ * @access public
50890
+ * @category Config/Replay
50891
+ * @name recording.recordCrossOriginIframes
50892
+ * @default true
50893
+ * @type {boolean}
50894
+ */
50895
+ // Like recording.enabled, this defaults to undefined so that it only overrides the
50896
+ // value returned by the backend when it is explicitly set to true/false
50897
+ cf.addOption(RECORDING_CONFIG_RECORD_CROSS_ORIGIN_IFRAMES, [cf.sources.SNIPPET_SRC], undefined);
50808
50898
  cf.addOption(RESOURCE_CACHING, [cf.sources.PENDO_CONFIG_SRC], undefined);
50809
50899
  }
50810
50900
  initialize(pendo, PluginAPI) {
@@ -51024,6 +51114,9 @@ class SessionRecorder {
51024
51114
  });
51025
51115
  const blockedSelectors = ['.pendo-ignore', '.pendo-sr-ignore'].concat(recordingOptions.blockedSelectors || []);
51026
51116
  const hiddenSelectors = ['.pendo-sr-hide'].concat(recordingOptions.hiddenSelectors || []);
51117
+ // The snippet config value should take precedence over the value from the visitor config
51118
+ const snippetValue = this.api.ConfigReader.get(RECORDING_CONFIG_RECORD_CROSS_ORIGIN_IFRAMES);
51119
+ const recordCrossOriginIframes = this.pendo._.isBoolean(snippetValue) ? snippetValue : recordingOptions.recordCrossOriginIframes;
51027
51120
  const config = {
51028
51121
  maskInputFn: this.pendo._.partial(maskInput, maskOptions),
51029
51122
  maskTextFn: this.pendo._.partial(maskText, maskOptions),
@@ -51034,7 +51127,7 @@ class SessionRecorder {
51034
51127
  // we need to concat hidden selectors to re-use block selector logic
51035
51128
  blockSelector: blockedSelectors.concat(hiddenSelectors).join(','),
51036
51129
  hideSelector: hiddenSelectors.join(','),
51037
- recordCrossOriginIframes: recordingOptions.recordCrossOriginIframes != null ? recordingOptions.recordCrossOriginIframes : true,
51130
+ recordCrossOriginIframes: recordCrossOriginIframes != null ? recordCrossOriginIframes : true,
51038
51131
  emitFromIframe: !!this.api.ConfigReader.get(RECORDING_CONFIG_TREAT_IFRAME_AS_ROOT)
51039
51132
  };
51040
51133
  return config;
@@ -51790,6 +51883,20 @@ var WorkerFactory = /*#__PURE__*/createInlineWorkerFactory(/* rollup-plugin-web-
51790
51883
  }
51791
51884
 
51792
51885
  const ONE_HUNDRED_MB_IN_BYTES = 100 * 1024 * 1024;
51886
+ // An undrained response body holds its stream, the underlying data pipe and the
51887
+ // socket open until the response is collected, which in a worker that lives for
51888
+ // the whole session may never happen. Deliberately duplicated from
51889
+ // cancelResponseBody in src/transmit.js instead of imported: this is its own
51890
+ // bundle, and importing transmit would pull the rest of its dependencies in.
51891
+ function cancelResponseBody(response) {
51892
+ try {
51893
+ if (response && response.body && typeof response.body.cancel === 'function') {
51894
+ response.body.cancel().catch(function () { });
51895
+ }
51896
+ }
51897
+ catch (e) {
51898
+ }
51899
+ }
51793
51900
  function matchHostedResources(recordingEvent, stringifiedRecordingPayload) {
51794
51901
  const fontURLRegex = /https:\/\/[^"\\\s]+?\.(woff2?|ttf|otf|eot)(\?[^"\\\s]*)?\b/g;
51795
51902
  const matches = stringifiedRecordingPayload.match(fontURLRegex);
@@ -51864,6 +51971,7 @@ var WorkerFactory = /*#__PURE__*/createInlineWorkerFactory(/* rollup-plugin-web-
51864
51971
  method: 'POST',
51865
51972
  body
51866
51973
  }).then(function (response) {
51974
+ cancelResponseBody(response);
51867
51975
  if (response.status < 200 || response.status >= 300) {
51868
51976
  postMessage({
51869
51977
  error: new Error(`received status code ${response.status}: ${response.statusText}`),
@@ -52623,7 +52731,10 @@ function ConsoleCapture() {
52623
52731
  let transport;
52624
52732
  let isPtmPaused;
52625
52733
  let isCapturingConsoleLogs = false;
52734
+ let alwaysCaptureErrors = false;
52735
+ let isRecordingActive = false;
52626
52736
  const CAPTURE_CONSOLE_CONFIG = 'captureConsoleLogs';
52737
+ const ALWAYS_CAPTURE_ERRORS_CONFIG = 'alwaysCaptureDevLogErrors';
52627
52738
  const CONSOLE_METHODS = ['log', 'warn', 'error', 'info'];
52628
52739
  const DEV_LOG_SUB_TYPE = 'console';
52629
52740
  // deduplicate logs
@@ -52662,9 +52773,11 @@ function ConsoleCapture() {
52662
52773
  pluginAPI = PluginAPI;
52663
52774
  const { ConfigReader } = pluginAPI;
52664
52775
  ConfigReader.addOption(CAPTURE_CONSOLE_CONFIG, [ConfigReader.sources.PENDO_CONFIG_SRC], false);
52776
+ ConfigReader.addOption(ALWAYS_CAPTURE_ERRORS_CONFIG, [ConfigReader.sources.PENDO_CONFIG_SRC], false);
52665
52777
  const captureConsoleEnabled = ConfigReader.get(CAPTURE_CONSOLE_CONFIG);
52666
52778
  if (!captureConsoleEnabled)
52667
52779
  return;
52780
+ alwaysCaptureErrors = !!ConfigReader.get(ALWAYS_CAPTURE_ERRORS_CONFIG);
52668
52781
  globalPendo = pendo;
52669
52782
  buffer = new DevlogBuffer(pendo, pluginAPI);
52670
52783
  transport = new DevlogTransport(pendo, pluginAPI);
@@ -52708,11 +52821,19 @@ function ConsoleCapture() {
52708
52821
  pluginAPI.log.info(`[ConsoleCapture] Console log capture ${shouldCapture ? 'started' : 'stopped'}${reason ? `: ${reason}` : ''}`);
52709
52822
  }
52710
52823
  function recordingStarted() {
52824
+ isRecordingActive = true;
52711
52825
  setCaptureState({ shouldCapture: true, reason: 'recording started' });
52712
52826
  }
52713
52827
  function recordingStopped() {
52828
+ isRecordingActive = false;
52829
+ if (alwaysCaptureErrors)
52830
+ return; // capture isn't tied to the recording lifecycle
52714
52831
  setCaptureState({ shouldCapture: false, reason: 'recording stopped' });
52715
52832
  }
52833
+ // ingestion only keeps error logs when there's no recording, so don't send the rest
52834
+ function isErrorOnlyCapture() {
52835
+ return alwaysCaptureErrors && !isRecordingActive;
52836
+ }
52716
52837
  function readyHandler() {
52717
52838
  try {
52718
52839
  addIntercepts();
@@ -52721,6 +52842,10 @@ function ConsoleCapture() {
52721
52842
  catch (error) {
52722
52843
  pluginAPI.log.error('Error setting up console capture intercepts: ', error);
52723
52844
  teardown();
52845
+ return;
52846
+ }
52847
+ if (alwaysCaptureErrors) {
52848
+ setCaptureState({ shouldCapture: true, reason: 'always capture dev log errors' });
52724
52849
  }
52725
52850
  }
52726
52851
  function addIntercepts() {
@@ -52756,6 +52881,8 @@ function ConsoleCapture() {
52756
52881
  function createConsoleEvent(args, methodName, { skipStackTrace = false, skipScrubPII = false } = {}) {
52757
52882
  if (!isCapturingConsoleLogs || !args || args.length === 0 || !_.contains(CONSOLE_METHODS, methodName))
52758
52883
  return;
52884
+ if (isErrorOnlyCapture() && methodName !== 'error')
52885
+ return;
52759
52886
  // stringify args
52760
52887
  let message = _.compact(_.map(args, arg => {
52761
52888
  let stringifiedArg;
@@ -52869,8 +52996,11 @@ function NetworkCapture() {
52869
52996
  let responseBodyCb;
52870
52997
  let pendoDevlogBaseUrl;
52871
52998
  let isCapturingNetworkLogs = false;
52999
+ let alwaysCaptureErrors = false;
53000
+ let isRecordingActive = false;
52872
53001
  let excludeRequestUrls = [];
52873
53002
  const CAPTURE_NETWORK_CONFIG = 'captureNetworkRequests';
53003
+ const ALWAYS_CAPTURE_ERRORS_CONFIG = 'alwaysCaptureDevLogErrors';
52874
53004
  const NETWORK_SUB_TYPE = 'network';
52875
53005
  const NETWORK_LOGS_CONFIG = 'networkLogs';
52876
53006
  const NETWORK_LOGS_CONFIG_ALLOWED_REQUEST_HEADERS = 'networkLogs.allowedRequestHeaders';
@@ -52946,9 +53076,11 @@ function NetworkCapture() {
52946
53076
  globalPendo = pendo;
52947
53077
  const { ConfigReader } = pluginAPI;
52948
53078
  ConfigReader.addOption(CAPTURE_NETWORK_CONFIG, [ConfigReader.sources.PENDO_CONFIG_SRC], false);
53079
+ ConfigReader.addOption(ALWAYS_CAPTURE_ERRORS_CONFIG, [ConfigReader.sources.PENDO_CONFIG_SRC], false);
52949
53080
  const captureNetworkEnabled = ConfigReader.get(CAPTURE_NETWORK_CONFIG);
52950
53081
  if (!captureNetworkEnabled)
52951
53082
  return;
53083
+ alwaysCaptureErrors = !!ConfigReader.get(ALWAYS_CAPTURE_ERRORS_CONFIG);
52952
53084
  buffer = new DevlogBuffer(pendo, pluginAPI);
52953
53085
  transport = new DevlogTransport(pendo, pluginAPI);
52954
53086
  sendQueue = new pluginAPI.SendQueue(transport.sendRequest.bind(transport));
@@ -53008,7 +53140,9 @@ function NetworkCapture() {
53008
53140
  */
53009
53141
  ConfigReader.addOption(NETWORK_LOGS_CONFIG_CAPTURE_REQUEST_BODY, [ConfigReader.sources.SNIPPET_SRC], undefined);
53010
53142
  /**
53011
- * Callback function to process/modify response bodies before capturing them. If not provided, the response body will not be captured.
53143
+ * Callback function to process/modify response bodies before capturing them. If not provided, the response body
53144
+ * will not be captured, unless `alwaysCaptureDevLogErrors` is enabled: error response bodies are then captured
53145
+ * as-is until a callback is provided.
53012
53146
  *
53013
53147
  * @access public
53014
53148
  * @category Config/Network Logs
@@ -53053,6 +53187,14 @@ function NetworkCapture() {
53053
53187
  responseBodyCb = config.captureResponseBody;
53054
53188
  }
53055
53189
  }
53190
+ // error bodies are captured by default here; processBody still masks and truncates them
53191
+ function getResponseBodyCb(response) {
53192
+ if (responseBodyCb)
53193
+ return responseBodyCb;
53194
+ if (alwaysCaptureErrors && response.status >= 400)
53195
+ return (body) => body;
53196
+ return undefined;
53197
+ }
53056
53198
  function buildExcludeRequestUrls() {
53057
53199
  const requestUrls = pluginAPI.ConfigReader.get(NETWORK_LOGS_CONFIG_EXCLUDE_REQUEST_URLS);
53058
53200
  if (!requestUrls || requestUrls.length === 0)
@@ -53100,13 +53242,24 @@ function NetworkCapture() {
53100
53242
  pluginAPI.log.info(`[NetworkCapture] Network request capture ${shouldCapture ? 'started' : 'stopped'}${reason ? `: ${reason}` : ''}`);
53101
53243
  }
53102
53244
  function recordingStarted() {
53245
+ isRecordingActive = true;
53103
53246
  return setCaptureState({ shouldCapture: true, reason: 'recording started' });
53104
53247
  }
53105
53248
  function recordingStopped() {
53249
+ isRecordingActive = false;
53250
+ if (alwaysCaptureErrors)
53251
+ return; // capture isn't tied to the recording lifecycle
53106
53252
  return setCaptureState({ shouldCapture: false, reason: 'recording stopped' });
53107
53253
  }
53254
+ // ingestion only keeps error responses when there's no recording, so don't send the rest
53255
+ function isErrorOnlyCapture() {
53256
+ return alwaysCaptureErrors && !isRecordingActive;
53257
+ }
53108
53258
  function startCapture() {
53109
53259
  pluginAPI.NetworkRequest.on({ request: handleRequest, response: handleResponse, error: handleError });
53260
+ if (alwaysCaptureErrors) {
53261
+ setCaptureState({ shouldCapture: true, reason: 'always capture dev log errors' });
53262
+ }
53110
53263
  }
53111
53264
  function teardown() {
53112
53265
  if (sendInterval) {
@@ -53154,6 +53307,10 @@ function NetworkCapture() {
53154
53307
  delete requestMap[response.requestId];
53155
53308
  return;
53156
53309
  }
53310
+ if (isErrorOnlyCapture() && !(response.status >= 400)) {
53311
+ delete requestMap[response.requestId];
53312
+ return;
53313
+ }
53157
53314
  if (!buffer.hasTokenAvailable()) {
53158
53315
  delete requestMap[response.requestId];
53159
53316
  return;
@@ -53212,11 +53369,11 @@ function NetworkCapture() {
53212
53369
  return '[Failed to process request body]';
53213
53370
  }
53214
53371
  }
53215
- function processResponseBody({ response }) {
53216
- if (!response || !response.body || !responseBodyCb)
53372
+ function processResponseBody({ response, bodyCb = responseBodyCb }) {
53373
+ if (!response || !response.body || !bodyCb)
53217
53374
  return '';
53218
53375
  try {
53219
- const body = responseBodyCb(response.body, { response });
53376
+ const body = bodyCb(response.body, { response });
53220
53377
  const contentType = globalPendo._.get(response, 'headers.content-type', '');
53221
53378
  return processBody(body, contentType);
53222
53379
  }
@@ -53233,8 +53390,9 @@ function NetworkCapture() {
53233
53390
  if (requestBodyCb) {
53234
53391
  networkEvent.devLogRequestBody = processRequestBody({ request });
53235
53392
  }
53236
- if (responseBodyCb) {
53237
- networkEvent.devLogResponseBody = processResponseBody({ response });
53393
+ const effectiveResponseBodyCb = getResponseBodyCb(response);
53394
+ if (effectiveResponseBodyCb) {
53395
+ networkEvent.devLogResponseBody = processResponseBody({ response, bodyCb: effectiveResponseBodyCb });
53238
53396
  }
53239
53397
  return networkEvent;
53240
53398
  }
@@ -55315,12 +55473,18 @@ const WIDGET_SETTINGS_API = '/external/widget/settings';
55315
55473
  class PredictApiService {
55316
55474
  constructor({ baseUrl, token }) {
55317
55475
  this.performRequest = (url, options) => __awaiter(this, void 0, void 0, function* () {
55476
+ var _a;
55318
55477
  const response = yield fetch(url, options);
55319
- const body = response.ok ? yield response.json() : null;
55320
- return { ok: response.ok, status: response.status, body };
55478
+ if (!response.ok) {
55479
+ // nothing reads an error body, and leaving it undrained holds its
55480
+ // stream and socket open until the response is collected
55481
+ (_a = response.body) === null || _a === void 0 ? void 0 : _a.cancel().catch(() => { });
55482
+ return { ok: false, status: response.status, body: null };
55483
+ }
55484
+ return { ok: true, status: response.status, body: yield response.json() };
55321
55485
  });
55322
55486
  this.getWidgetSettingsById = (id, log) => __awaiter(this, void 0, void 0, function* () {
55323
- var _a, _b;
55487
+ var _b, _c;
55324
55488
  if (!id) {
55325
55489
  log('[predict] no id provided');
55326
55490
  return null;
@@ -55335,11 +55499,11 @@ class PredictApiService {
55335
55499
  return null;
55336
55500
  }
55337
55501
  const data = body;
55338
- if ((_a = data === null || data === void 0 ? void 0 : data.errors) === null || _a === void 0 ? void 0 : _a.length) {
55502
+ if ((_b = data === null || data === void 0 ? void 0 : data.errors) === null || _b === void 0 ? void 0 : _b.length) {
55339
55503
  log(`[predict] error getting widget settings by id: ${data.errors}`);
55340
55504
  return null;
55341
55505
  }
55342
- return (_b = data === null || data === void 0 ? void 0 : data.data) !== null && _b !== void 0 ? _b : null;
55506
+ return (_c = data === null || data === void 0 ? void 0 : data.data) !== null && _c !== void 0 ? _c : null;
55343
55507
  }
55344
55508
  catch (error) {
55345
55509
  log(`[predict] error getting widget settings by id: ${error}`);