@pendo/agent 2.350.0 → 2.350.2

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.
package/dist/dom.esm.js CHANGED
@@ -7592,7 +7592,7 @@ function applyMatrix2dRect(matrix2d, rect) {
7592
7592
  return transformedRect;
7593
7593
  }
7594
7594
 
7595
- var VERSION = '2.350.0_';
7595
+ var VERSION = '2.350.2_';
7596
7596
 
7597
7597
  var decodeURIComponent = _.isFunction(globalScope.decodeURIComponent) ? globalScope.decodeURIComponent : _.identity;
7598
7598
 
@@ -4175,8 +4175,8 @@ let SERVER = '';
4175
4175
  let ASSET_HOST = '';
4176
4176
  let ASSET_PATH = '';
4177
4177
  let DESIGNER_SERVER = '';
4178
- let VERSION = '2.350.0_';
4179
- let PACKAGE_VERSION = '2.350.0';
4178
+ let VERSION = '2.350.2_';
4179
+ let PACKAGE_VERSION = '2.350.2';
4180
4180
  let LOADER = 'xhr';
4181
4181
  /* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
4182
4182
  /**
@@ -12686,6 +12686,139 @@ var getURL = function () {
12686
12686
  return pendoDotUrl.get();
12687
12687
  };
12688
12688
 
12689
+ const UNSENT_EVENTS_KEY = 'unsentEvents';
12690
+ const UNSENT_EVENTS_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
12691
+ function mergeEvents(target, source) {
12692
+ _.each(source, (requests, key) => {
12693
+ target[key] = (target[key] || []).concat(requests);
12694
+ });
12695
+ return target;
12696
+ }
12697
+ function migrateLegacyCookieEvents(events) {
12698
+ const cookieValue = get_pendo_cookie(UNSENT_EVENTS_KEY);
12699
+ if (cookieValue) {
12700
+ try {
12701
+ mergeEvents(events, JSON.parse(cookieValue));
12702
+ }
12703
+ catch (e) { }
12704
+ clearCookie(getPendoCookieKey(UNSENT_EVENTS_KEY));
12705
+ }
12706
+ }
12707
+ // the quota error doesn't say how much space is left, so halving keeps the
12708
+ // number of retries during pagehide small
12709
+ function dropOldestHalf(events) {
12710
+ const entries = [];
12711
+ _.each(events, (requests, key) => {
12712
+ _.each(requests, (request) => {
12713
+ entries.push({ key, request });
12714
+ });
12715
+ });
12716
+ const newest = _.sortBy(entries, (entry) => entry.request.ts).slice(Math.ceil(entries.length / 2));
12717
+ const remaining = {};
12718
+ _.each(newest, (entry) => {
12719
+ remaining[entry.key] = remaining[entry.key] || [];
12720
+ remaining[entry.key].push(entry.request);
12721
+ });
12722
+ return remaining;
12723
+ }
12724
+ class LocalStorageEventBuffer {
12725
+ constructor() {
12726
+ this.events = {};
12727
+ this.saved = [];
12728
+ }
12729
+ send(key, queue) {
12730
+ const data = this.events[key];
12731
+ if (!data)
12732
+ return;
12733
+ let request = data.shift();
12734
+ while (request) {
12735
+ queue.push(request);
12736
+ request = data.shift();
12737
+ }
12738
+ delete this.events[key];
12739
+ }
12740
+ save(key, sendQueue) {
12741
+ const previous = _.find(this.saved, (entry) => entry.sendQueue === sendQueue);
12742
+ if (!previous && sendQueue.queue.length === 0)
12743
+ return;
12744
+ this.saved = _.without(this.saved, previous);
12745
+ if (sendQueue.queue.length > 0) {
12746
+ this.saved.push({ key, sendQueue, requests: sendQueue.queue.slice() });
12747
+ }
12748
+ this.dirty = true;
12749
+ }
12750
+ writeIfDirty() {
12751
+ if (this.dirty) {
12752
+ this.write();
12753
+ }
12754
+ }
12755
+ remove(sendQueue, request) {
12756
+ const entry = _.find(this.saved, (candidate) => candidate.sendQueue === sendQueue);
12757
+ if (!entry)
12758
+ return;
12759
+ const index = _.indexOf(entry.requests, request);
12760
+ if (index < 0)
12761
+ return;
12762
+ entry.requests.splice(index, 1);
12763
+ if (entry.requests.length === 0) {
12764
+ this.saved = _.without(this.saved, entry);
12765
+ }
12766
+ // rewriting the whole copy after every send is quadratic while a long backlog drains, so leave
12767
+ // it for the next flushNow
12768
+ this.dirty = true;
12769
+ }
12770
+ read(storage) {
12771
+ /**
12772
+ * If enabled, Pendo will write pending events to localStorage before unloading the page
12773
+ * rather than attempting to send them. The events will be sent the next time the page is loaded.
12774
+ *
12775
+ * @name _pendo_unsentEvents
12776
+ * @category Cookies/localStorage
12777
+ * @access public
12778
+ * @label UNSENT_EVENTS_KEY
12779
+ */
12780
+ storage.registry.addLocal(UNSENT_EVENTS_KEY, {
12781
+ localStorageOnly: true,
12782
+ duration: UNSENT_EVENTS_MAX_AGE
12783
+ });
12784
+ try {
12785
+ this.events = JSON.parse(storage.read(UNSENT_EVENTS_KEY) || '{}');
12786
+ }
12787
+ catch (e) {
12788
+ this.events = {};
12789
+ }
12790
+ migrateLegacyCookieEvents(this.events);
12791
+ storage.clear(UNSENT_EVENTS_KEY);
12792
+ this.storage = storage;
12793
+ this.saved = [];
12794
+ this.dirty = false;
12795
+ }
12796
+ // requests still in flight at teardown can finish afterwards, and a flushNow after that would
12797
+ // write the saved copy under a key built from the now-cleared apiKey
12798
+ teardown() {
12799
+ this.storage = undefined;
12800
+ this.saved = [];
12801
+ this.dirty = false;
12802
+ }
12803
+ write(storage = this.storage) {
12804
+ if (!storage)
12805
+ return;
12806
+ this.dirty = false;
12807
+ let events = mergeEvents({}, this.events);
12808
+ _.each(this.saved, (entry) => {
12809
+ mergeEvents(events, { [entry.key]: entry.requests });
12810
+ });
12811
+ while (_.size(events) > 0) {
12812
+ if (storage.write(UNSENT_EVENTS_KEY, JSON.stringify(events), UNSENT_EVENTS_MAX_AGE)) {
12813
+ return;
12814
+ }
12815
+ events = dropOldestHalf(events);
12816
+ }
12817
+ storage.clear(UNSENT_EVENTS_KEY);
12818
+ }
12819
+ }
12820
+ var localStorageEventBuffer = new LocalStorageEventBuffer();
12821
+
12689
12822
  var buffers = {
12690
12823
  /* : events: Event[] */
12691
12824
  /* : silos: Silo[] */
@@ -12749,7 +12882,7 @@ function callLater(callback, n) {
12749
12882
  * @access public
12750
12883
  * @category Events
12751
12884
  * @param {Object} [options] - Options passed through to each queue's flush method
12752
- * @param {boolean} [options.unload] - signals the page is unloading; drains the send queue using sendBeacon or sync XHR for reliable last-chance delivery
12885
+ * @param {boolean} [options.unload] - signals the page is unloading; drains the send queue using sendBeacon or sync XHR for reliable last-chance delivery. With `analytics.localStorageUnload` enabled, unsent events are saved to localStorage instead and sent on the next page load, or on the next flush if the page doesn't unload
12753
12886
  * @param {boolean} [options.hidden] - signals the page is being hidden; drains the send queue immediately rather than waiting for the next timer tick
12754
12887
  * @returns {void}
12755
12888
  * @example
@@ -12762,6 +12895,7 @@ function flushNow(options) {
12762
12895
  queue.flush(options);
12763
12896
  }
12764
12897
  });
12898
+ localStorageEventBuffer.writeIfDirty();
12765
12899
  }
12766
12900
  catch (e) {
12767
12901
  log.critical('unhandled error while flushing event cache', { error: e });
@@ -12927,9 +13061,12 @@ class SendQueue {
12927
13061
  this.stopped = false;
12928
13062
  }
12929
13063
  push(...payload) {
12930
- this.queue.push(...payload);
13064
+ this.enqueue(...payload);
12931
13065
  this.next();
12932
13066
  }
13067
+ enqueue(...payload) {
13068
+ this.queue.push(...payload);
13069
+ }
12933
13070
  next() {
12934
13071
  if (this.queue.length && this.pending.size < 1) {
12935
13072
  const nextPayload = this.queue[0];
@@ -12953,6 +13090,9 @@ class SendQueue {
12953
13090
  if (index >= 0) {
12954
13091
  this.queue.splice(index, 1);
12955
13092
  }
13093
+ if (this.onPass) {
13094
+ this.onPass(payload);
13095
+ }
12956
13096
  if (!this.stopped && dequeue) {
12957
13097
  this.next();
12958
13098
  }
@@ -13015,97 +13155,6 @@ class SendQueue {
13015
13155
  }
13016
13156
  }
13017
13157
 
13018
- const UNSENT_EVENTS_KEY = 'unsentEvents';
13019
- const UNSENT_EVENTS_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
13020
- function mergeEvents(target, source) {
13021
- _.each(source, (requests, key) => {
13022
- target[key] = (target[key] || []).concat(requests);
13023
- });
13024
- return target;
13025
- }
13026
- function migrateLegacyCookieEvents(events) {
13027
- const cookieValue = get_pendo_cookie(UNSENT_EVENTS_KEY);
13028
- if (cookieValue) {
13029
- try {
13030
- mergeEvents(events, JSON.parse(cookieValue));
13031
- }
13032
- catch (e) { }
13033
- clearCookie(getPendoCookieKey(UNSENT_EVENTS_KEY));
13034
- }
13035
- }
13036
- // the quota error doesn't say how much space is left, so halving keeps the
13037
- // number of retries during pagehide small
13038
- function dropOldestHalf(events) {
13039
- const entries = [];
13040
- _.each(events, (requests, key) => {
13041
- _.each(requests, (request) => {
13042
- entries.push({ key, request });
13043
- });
13044
- });
13045
- const newest = _.sortBy(entries, (entry) => entry.request.ts).slice(Math.ceil(entries.length / 2));
13046
- const remaining = {};
13047
- _.each(newest, (entry) => {
13048
- remaining[entry.key] = remaining[entry.key] || [];
13049
- remaining[entry.key].push(entry.request);
13050
- });
13051
- return remaining;
13052
- }
13053
- class LocalStorageEventBuffer {
13054
- constructor() {
13055
- this.events = {};
13056
- }
13057
- send(key, queue) {
13058
- const data = this.events[key];
13059
- if (!data)
13060
- return;
13061
- let request = data.shift();
13062
- while (request) {
13063
- queue.push(request);
13064
- request = data.shift();
13065
- }
13066
- delete this.events[key];
13067
- }
13068
- push(key, obj) {
13069
- const data = this.events[key] || [];
13070
- data.push(obj);
13071
- this.events[key] = data;
13072
- }
13073
- read(storage) {
13074
- /**
13075
- * If enabled, Pendo will write pending events to localStorage before unloading the page
13076
- * rather than attempting to send them. The events will be sent the next time the page is loaded.
13077
- *
13078
- * @name _pendo_unsentEvents
13079
- * @category Cookies/localStorage
13080
- * @access public
13081
- * @label UNSENT_EVENTS_KEY
13082
- */
13083
- storage.registry.addLocal(UNSENT_EVENTS_KEY, {
13084
- localStorageOnly: true,
13085
- duration: UNSENT_EVENTS_MAX_AGE
13086
- });
13087
- try {
13088
- this.events = JSON.parse(storage.read(UNSENT_EVENTS_KEY) || '{}');
13089
- }
13090
- catch (e) {
13091
- this.events = {};
13092
- }
13093
- migrateLegacyCookieEvents(this.events);
13094
- storage.clear(UNSENT_EVENTS_KEY);
13095
- }
13096
- write(storage) {
13097
- let events = this.events;
13098
- while (_.size(events) > 0) {
13099
- if (storage.write(UNSENT_EVENTS_KEY, JSON.stringify(events), UNSENT_EVENTS_MAX_AGE)) {
13100
- return;
13101
- }
13102
- events = dropOldestHalf(events);
13103
- }
13104
- storage.clear(UNSENT_EVENTS_KEY);
13105
- }
13106
- }
13107
- var localStorageEventBuffer = new LocalStorageEventBuffer();
13108
-
13109
13158
  /*
13110
13159
  * These implementations are meant to help keep the agent's metrics usage aligned with
13111
13160
  * the expectations for the backend's open telemetry.
@@ -13807,6 +13856,10 @@ class SendQueueSplitter {
13807
13856
  const args = _.toArray(arguments);
13808
13857
  _.each(this.queues, (queue) => queue.push.apply(queue, args));
13809
13858
  }
13859
+ enqueue() {
13860
+ const args = _.toArray(arguments);
13861
+ _.each(this.queues, (queue) => queue.enqueue.apply(queue, args));
13862
+ }
13810
13863
  drain() {
13811
13864
  const args = _.toArray(arguments);
13812
13865
  return q.all(_.map(this.queues, (queue) => queue.drain.apply(queue, args)));
@@ -13826,12 +13879,6 @@ function createSendQueue(options, send, guaranteedSend) {
13826
13879
  rt: failureCount
13827
13880
  });
13828
13881
  }
13829
- if (options.localStorageUnload && isUnload) {
13830
- if (i === 0) {
13831
- localStorageEventBuffer.push(options.beacon, request);
13832
- }
13833
- return q.resolve();
13834
- }
13835
13882
  if (isUnload && queue.failed()) {
13836
13883
  return q.reject();
13837
13884
  }
@@ -13845,6 +13892,11 @@ function createSendQueue(options, send, guaranteedSend) {
13845
13892
  queue.onTimeout = function () {
13846
13893
  performanceMonitor.count(BEACON_GIF_FAILURES[options.beacon]);
13847
13894
  };
13895
+ if (options.localStorageUnload && i === 0) {
13896
+ queue.onPass = function (request) {
13897
+ localStorageEventBuffer.remove(queue, request);
13898
+ };
13899
+ }
13848
13900
  queue.allowDrainWhileFailed = true;
13849
13901
  queue.retryPending = true;
13850
13902
  return queue;
@@ -13876,6 +13928,8 @@ class EventQueue {
13876
13928
  this.silos = options.silos || [];
13877
13929
  this.packageSilos = options.packageSilos;
13878
13930
  this.processSilos = options.processSilos;
13931
+ this.beacon = options.beacon;
13932
+ this.localStorageUnload = options.localStorageUnload;
13879
13933
  this.sendQueue = createSendQueue(options);
13880
13934
  }
13881
13935
  // silent skips the ptm lifecycle events, for a queue paused alongside the main one so they fire once.
@@ -13911,6 +13965,9 @@ class EventQueue {
13911
13965
  }
13912
13966
  flush({ unload = false, hidden = false } = {}) {
13913
13967
  const { cache, silos } = this;
13968
+ if (!unload && !hidden) {
13969
+ this.savedOnUnload = false;
13970
+ }
13914
13971
  if (cache.length === 0 && silos.length === 0 && this.sendQueue.isEmpty())
13915
13972
  return;
13916
13973
  const leavingPage = unload || hidden;
@@ -13926,7 +13983,15 @@ class EventQueue {
13926
13983
  requests.push(silo);
13927
13984
  });
13928
13985
  }, this);
13929
- if (unload || hidden) {
13986
+ // queued but not sent: if the page stays alive the next flush sends them, and each one that goes out is
13987
+ // removed from the saved copy. a real unload is followed by a hidden flush (unload/pagehide fire before
13988
+ // visibilitychange), which has to save as well rather than send everything that was just saved
13989
+ if (this.localStorageUnload && (unload || (hidden && this.savedOnUnload))) {
13990
+ this.savedOnUnload = true;
13991
+ this.sendQueue.enqueue(...requests);
13992
+ localStorageEventBuffer.save(this.beacon, this.sendQueue.queues[0]);
13993
+ }
13994
+ else if (unload || hidden) {
13930
13995
  this.sendQueue.drain(requests, unload);
13931
13996
  }
13932
13997
  else {
@@ -31341,6 +31406,7 @@ function initialize(options) {
31341
31406
  }
31342
31407
  const localStorageUnloadEnabled = ConfigReader.get('analytics.localStorageUnload');
31343
31408
  localStorageEventBuffer.read(agentStorage);
31409
+ teardownFns.push(() => localStorageEventBuffer.teardown());
31344
31410
  teardownFns.push(initializeEventBuffer({
31345
31411
  localStorageUnload: localStorageUnloadEnabled
31346
31412
  }));
@@ -31410,9 +31476,6 @@ function initialize(options) {
31410
31476
  teardownFns.push(() => Events.appHidden.off(flushOnAppHidden));
31411
31477
  const flushOnAppUnloaded = () => {
31412
31478
  flushNow({ unload: true });
31413
- if (localStorageUnloadEnabled) {
31414
- localStorageEventBuffer.write(agentStorage);
31415
- }
31416
31479
  };
31417
31480
  Events.appUnloaded.on(flushOnAppUnloaded);
31418
31481
  teardownFns.push(() => Events.appUnloaded.off(flushOnAppUnloaded));
@@ -57418,9 +57481,13 @@ const injectStyles = (pendoContainerId, log = () => { }) => {
57418
57481
  `
57419
57482
  }, document.head);
57420
57483
  };
57421
- const getRecordIdFromUrl = (recordRegex, log = () => { }) => {
57484
+ // Guides can render inside an iframe whose own location differs from the page
57485
+ // the SDK tracks, so prefer the SDK's URL (the one guides were loaded for).
57486
+ const getPendoPageUrl = (pendo) => { var _a, _b; return ((_b = (_a = pendo === null || pendo === void 0 ? void 0 : pendo.url) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a)) || window.location.href; };
57487
+ const stripOrigin = (href) => href.replace(/^[a-z][a-z\d+.-]*:\/\/[^/?#]*/i, '');
57488
+ const getRecordIdFromUrl = (recordRegex, href = window.location.href, log = () => { }) => {
57422
57489
  var _a;
57423
- const match = window.location.href.match(recordRegex);
57490
+ const match = href.match(recordRegex);
57424
57491
  const recordId = (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : null;
57425
57492
  log(`[predict] recordId found: ${recordId}`);
57426
57493
  return recordId;
@@ -57438,13 +57505,13 @@ const buildRecordRegex = (keyword) => new RegExp(`(?:^|[/?&=.#])${escapeRegexKey
57438
57505
  // 2. recordRegexName - the generalized keyword matcher (path + query).
57439
57506
  // 3. legacy fallback - the original strict `/name/id` template, so every
57440
57507
  // pre-existing config keeps working with no migration.
57441
- const resolveRecordId = (configuration, log = () => { }) => {
57508
+ const resolveRecordId = (configuration, href = window.location.href, log = () => { }) => {
57442
57509
  var _a, _b, _c, _d;
57443
57510
  const { recordRegexPattern, recordRegexName } = configuration;
57444
57511
  // Tier 1: advanced full pattern (matched against the full href).
57445
57512
  if (recordRegexPattern) {
57446
57513
  try {
57447
- const advancedId = (_b = (_a = window.location.href.match(new RegExp(recordRegexPattern))) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null;
57514
+ const advancedId = (_b = (_a = href.match(new RegExp(recordRegexPattern))) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null;
57448
57515
  if (advancedId) {
57449
57516
  log(`[predict] recordId found via recordRegexPattern: ${advancedId}`);
57450
57517
  return advancedId;
@@ -57459,15 +57526,14 @@ const resolveRecordId = (configuration, log = () => { }) => {
57459
57526
  }
57460
57527
  // Tier 2: generalized keyword matcher against path + query + hash only
57461
57528
  // (host excluded to avoid subdomain false matches).
57462
- const { pathname, search, hash } = window.location;
57463
- const keywordId = (_d = (_c = `${pathname}${search}${hash}`.match(buildRecordRegex(recordRegexName))) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : null;
57529
+ const keywordId = (_d = (_c = stripOrigin(href).match(buildRecordRegex(recordRegexName))) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : null;
57464
57530
  if (keywordId) {
57465
57531
  log(`[predict] recordId found via recordRegexName: ${keywordId}`);
57466
57532
  return keywordId;
57467
57533
  }
57468
57534
  // Tier 3: legacy template fallback (preserves pre-existing configs).
57469
57535
  const legacyRegex = new RegExp(`/${recordRegexName}/([a-zA-Z0-9]+)(?:/|$)`);
57470
- return getRecordIdFromUrl(legacyRegex, log);
57536
+ return getRecordIdFromUrl(legacyRegex, href, log);
57471
57537
  };
57472
57538
  const safeStringify = (value, fallback) => {
57473
57539
  try {
@@ -57814,7 +57880,9 @@ const predictGuidesScript = ({ step, pendo, cleanupArray, cleanup, log, designer
57814
57880
  predictBaseUrl,
57815
57881
  log }))
57816
57882
  : inlineConfiguration;
57817
- const recordId = resolveRecordId(configuration, log);
57883
+ const pageUrl = getPendoPageUrl(pendo);
57884
+ log(`[predict] resolving recordId from ${pageUrl}`);
57885
+ const recordId = resolveRecordId(configuration, pageUrl, log);
57818
57886
  if (!recordId) {
57819
57887
  log('[predict] no recordId found in URL, aborting');
57820
57888
  return;