@newrelic/browser-agent 1.297.1-rc.0 → 1.297.1-rc.1

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.
Files changed (46) hide show
  1. package/dist/cjs/common/constants/env.cdn.js +1 -1
  2. package/dist/cjs/common/constants/env.npm.js +1 -1
  3. package/dist/cjs/common/wrap/wrap-function.js +9 -4
  4. package/dist/cjs/features/ajax/aggregate/index.js +10 -2
  5. package/dist/cjs/features/ajax/instrument/index.js +1 -0
  6. package/dist/cjs/features/jserrors/aggregate/index.js +9 -4
  7. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +11 -3
  8. package/dist/cjs/features/soft_navigations/aggregate/index.js +38 -14
  9. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +34 -20
  10. package/dist/cjs/features/soft_navigations/constants.js +8 -4
  11. package/dist/cjs/features/soft_navigations/instrument/index.js +9 -6
  12. package/dist/esm/common/constants/env.cdn.js +1 -1
  13. package/dist/esm/common/constants/env.npm.js +1 -1
  14. package/dist/esm/common/wrap/wrap-function.js +9 -4
  15. package/dist/esm/features/ajax/aggregate/index.js +10 -2
  16. package/dist/esm/features/ajax/instrument/index.js +1 -0
  17. package/dist/esm/features/jserrors/aggregate/index.js +9 -4
  18. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +11 -3
  19. package/dist/esm/features/soft_navigations/aggregate/index.js +39 -15
  20. package/dist/esm/features/soft_navigations/aggregate/interaction.js +35 -21
  21. package/dist/esm/features/soft_navigations/constants.js +7 -3
  22. package/dist/esm/features/soft_navigations/instrument/index.js +10 -7
  23. package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
  24. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  25. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  26. package/dist/types/features/jserrors/aggregate/index.d.ts +1 -1
  27. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  28. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +2 -1
  29. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  30. package/dist/types/features/soft_navigations/aggregate/index.d.ts +1 -1
  31. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  32. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +6 -3
  33. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  34. package/dist/types/features/soft_navigations/constants.d.ts +4 -0
  35. package/dist/types/features/soft_navigations/constants.d.ts.map +1 -1
  36. package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/common/wrap/wrap-function.js +9 -4
  39. package/src/features/ajax/aggregate/index.js +10 -2
  40. package/src/features/ajax/instrument/index.js +1 -0
  41. package/src/features/jserrors/aggregate/index.js +10 -6
  42. package/src/features/soft_navigations/aggregate/ajax-node.js +8 -4
  43. package/src/features/soft_navigations/aggregate/index.js +39 -15
  44. package/src/features/soft_navigations/aggregate/interaction.js +33 -19
  45. package/src/features/soft_navigations/constants.js +5 -2
  46. package/src/features/soft_navigations/instrument/index.js +9 -8
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.297.1-rc.0";
20
+ const VERSION = exports.VERSION = "1.297.1-rc.1";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.297.1-rc.0";
20
+ const VERSION = exports.VERSION = "1.297.1-rc.1";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -18,6 +18,7 @@ var _bundleId = require("../ids/bundle-id");
18
18
  */
19
19
 
20
20
  const flag = exports.flag = "nr@original:".concat(_bundleId.bundleId);
21
+ const LONG_TASK_THRESHOLD = 50;
21
22
 
22
23
  /**
23
24
  * A convenience alias of `hasOwnProperty`.
@@ -94,7 +95,7 @@ function createWrapperWithEmitter(emitter, always) {
94
95
  // Warning: start events may mutate args!
95
96
  safeEmit(prefix + 'start', [args, originalThis, methodName], ctx, bubble);
96
97
  const fnStartTime = performance.now();
97
- let fnEndTime = fnStartTime;
98
+ let fnEndTime;
98
99
  try {
99
100
  result = fn.apply(originalThis, args);
100
101
  fnEndTime = performance.now();
@@ -108,16 +109,20 @@ function createWrapperWithEmitter(emitter, always) {
108
109
  } finally {
109
110
  const duration = fnEndTime - fnStartTime;
110
111
  const task = {
112
+ start: fnStartTime,
113
+ end: fnEndTime,
111
114
  duration,
112
- isLongTask: duration >= 50,
115
+ isLongTask: duration >= LONG_TASK_THRESHOLD,
113
116
  methodName,
114
117
  thrownError
115
118
  // could add more properties here later if needed by downstream features
116
119
  };
117
120
  // standalone long task message
118
- if (task.isLongTask) safeEmit('long-task', [task], ctx, bubble);
121
+ if (task.isLongTask) {
122
+ safeEmit('long-task', [task, originalThis], ctx, bubble);
123
+ }
119
124
  // -end message also includes the task execution info
120
- safeEmit(prefix + 'end', [args, originalThis, result, task], ctx, bubble);
125
+ safeEmit(prefix + 'end', [args, originalThis, result], ctx, bubble);
121
126
  }
122
127
  }
123
128
  }
@@ -13,6 +13,7 @@ var _features = require("../../../loaders/features/features");
13
13
  var _aggregateBase = require("../../utils/aggregate-base");
14
14
  var _gql = require("./gql");
15
15
  var _belSerializer = require("../../../common/serialize/bel-serializer");
16
+ var _nreum = require("../../../common/window/nreum");
16
17
  /**
17
18
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
18
19
  * SPDX-License-Identifier: Apache-2.0
@@ -49,6 +50,13 @@ class Aggregate extends _aggregateBase.AggregateBase {
49
50
  // the EE-drain system not only switches "this" but also passes a new EventContext with info. Should consider platform refactor to another system which passes a mutable context around separately and predictably to avoid problems like this.
50
51
  classThis.storeXhr(...arguments, this); // this switches the context back to the class instance while passing the NR context as an argument -- see "ctx" in storeXhr
51
52
  }, this.featureName, this.ee);
53
+ this.ee.on('long-task', (task, originator) => {
54
+ if (originator instanceof (0, _nreum.gosNREUMOriginals)().o.XHR) {
55
+ // any time a long task from XHR callback is observed, update the end time for soft nav use
56
+ const xhrMetadata = this.ee.context(originator);
57
+ xhrMetadata.latestLongtaskEnd = task.end;
58
+ }
59
+ });
52
60
  this.waitForFlags([]).then(() => this.drain());
53
61
  }
54
62
  storeXhr(params, metrics, startTime, endTime, type, ctx) {
@@ -108,8 +116,8 @@ class Aggregate extends _aggregateBase.AggregateBase {
108
116
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', (0, _stringify.stringify)(event.gql).length);
109
117
  const softNavInUse = Boolean(this.agentRef.features?.[_features.FEATURE_NAMES.softNav]);
110
118
  if (softNavInUse) {
111
- // For newer soft nav (when running), pass the event to it for evaluation -- either part of an interaction or is given back
112
- (0, _handle.handle)('ajax', [event], undefined, _features.FEATURE_NAMES.softNav, this.ee);
119
+ // For newer soft nav (when running), pass the event w/ info to it for evaluation -- either part of an interaction or is given back
120
+ (0, _handle.handle)('ajax', [event, ctx], undefined, _features.FEATURE_NAMES.softNav, this.ee);
113
121
  } else if (ctx.spaNode) {
114
122
  // For old spa (when running), if the ajax happened inside an interaction, hold it until the interaction finishes
115
123
  const interactionId = ctx.spaNode.interaction.id;
@@ -106,6 +106,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
106
106
  ctx.loadCaptureCalled = false;
107
107
  ctx.params = this.params || {};
108
108
  ctx.metrics = this.metrics || {};
109
+ ctx.latestLongtaskEnd = 0;
109
110
  xhr.addEventListener('load', function (event) {
110
111
  captureXhrData(ctx, xhr);
111
112
  }, (0, _eventListenerOpts.eventListenerOpts)(false));
@@ -44,7 +44,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
44
44
  this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved));
45
45
  (0, _registerHandler.registerHandler)('err', (...args) => this.storeError(...args), this.featureName, this.ee);
46
46
  (0, _registerHandler.registerHandler)('ierr', (...args) => this.storeError(...args), this.featureName, this.ee);
47
- (0, _registerHandler.registerHandler)('softNavFlush', (interactionId, wasFinished, softNavAttrs) => this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs), this.featureName, this.ee); // when an ixn is done or cancelled
47
+ (0, _registerHandler.registerHandler)('softNavFlush', (interactionId, wasFinished, softNavAttrs, interactionEndTime) => this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs, interactionEndTime), this.featureName, this.ee); // when an ixn is done or cancelled
48
48
 
49
49
  this.harvestOpts.aggregatorTypes = ['err', 'ierr', 'xhr']; // the types in EventAggregator this feature cares about
50
50
 
@@ -271,10 +271,15 @@ class Aggregate extends _aggregateBase.AggregateBase {
271
271
  });
272
272
  delete this.bufferedErrorsUnderSpa[interaction.id];
273
273
  }
274
- onSoftNavNotification(interactionId, wasFinished, softNavAttrs) {
274
+ onSoftNavNotification(interactionId, wasFinished, softNavAttrs, interactionEndTime) {
275
275
  if (this.blocked) return;
276
- this.bufferedErrorsUnderSpa[interactionId]?.forEach(jsErrorEvent => this.#storeJserrorForHarvest(jsErrorEvent, wasFinished, softNavAttrs) // this should not modify the re-used softNavAttrs contents
277
- );
276
+ this.bufferedErrorsUnderSpa[interactionId]?.forEach(jsErrorEvent => {
277
+ // this should not modify the re-used softNavAttrs contents
278
+ if (!wasFinished) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs);
279
+ const startTime = jsErrorEvent[3].time; // in storeError fn, the newMetrics obj contains the time passed to & used by SN to seek the ixn
280
+ if (startTime > interactionEndTime) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs); // disassociate any error that ultimately falls outside the final ixn span
281
+ return this.#storeJserrorForHarvest(jsErrorEvent, true, softNavAttrs);
282
+ });
278
283
  delete this.bufferedErrorsUnderSpa[interactionId]; // wipe the list of jserrors so they aren't duplicated by another call to the same id
279
284
  }
280
285
  }
@@ -13,7 +13,7 @@ var _belNode = require("./bel-node");
13
13
  */
14
14
 
15
15
  class AjaxNode extends _belNode.BelNode {
16
- constructor(ajaxEvent) {
16
+ constructor(ajaxEvent, ajaxContext) {
17
17
  super();
18
18
  this.belType = _constants.NODE_TYPE.AJAX;
19
19
  this.method = ajaxEvent.method;
@@ -27,8 +27,12 @@ class AjaxNode extends _belNode.BelNode {
27
27
  this.traceId = ajaxEvent.traceId;
28
28
  this.spanTimestamp = ajaxEvent.spanTimestamp;
29
29
  this.gql = ajaxEvent.gql;
30
- this.start = ajaxEvent.startTime; // 5000 --- 5500 --> 10500
30
+ this.start = ajaxEvent.startTime;
31
31
  this.end = ajaxEvent.endTime;
32
+ if (ajaxContext?.latestLongtaskEnd) {
33
+ this.callbackEnd = Math.max(ajaxContext.latestLongtaskEnd, this.end); // typically lt end if non-zero, but added clamping to end just in case
34
+ this.callbackDuration = this.callbackEnd - this.end; // callbackDuration is the time from ajax loaded to last long task observed from it
35
+ } else this.callbackEnd = this.end; // if no long task was observed, callbackEnd is the same as end
32
36
  }
33
37
  serialize(parentStartTimestamp, agentRef) {
34
38
  const addString = (0, _belSerializer.getAddStringContext)(agentRef.runtime.obfuscator);
@@ -41,7 +45,11 @@ class AjaxNode extends _belNode.BelNode {
41
45
  // start relative to parent start (if part of first node in payload) or first parent start
42
46
  (0, _belSerializer.numeric)(this.end - this.start),
43
47
  // end is relative to start
44
- (0, _belSerializer.numeric)(this.callbackEnd), (0, _belSerializer.numeric)(this.callbackDuration), addString(this.method), (0, _belSerializer.numeric)(this.status), addString(this.domain), addString(this.path), (0, _belSerializer.numeric)(this.txSize), (0, _belSerializer.numeric)(this.rxSize), this.requestedWith, addString(this.nodeId), (0, _belSerializer.nullable)(this.spanId, addString, true) + (0, _belSerializer.nullable)(this.traceId, addString, true) + (0, _belSerializer.nullable)(this.spanTimestamp, _belSerializer.numeric)];
48
+ (0, _belSerializer.numeric)(this.callbackEnd - this.end),
49
+ // callbackEnd is relative to end
50
+ (0, _belSerializer.numeric)(this.callbackDuration),
51
+ // not relative
52
+ addString(this.method), (0, _belSerializer.numeric)(this.status), addString(this.domain), addString(this.path), (0, _belSerializer.numeric)(this.txSize), (0, _belSerializer.numeric)(this.rxSize), this.requestedWith, addString(this.nodeId), (0, _belSerializer.nullable)(this.spanId, addString, true) + (0, _belSerializer.nullable)(this.traceId, addString, true) + (0, _belSerializer.nullable)(this.spanTimestamp, _belSerializer.numeric)];
45
53
  let allAttachedNodes = [];
46
54
  if (typeof this.gql === 'object') allAttachedNodes = (0, _belSerializer.addCustomAttributes)(this.gql, addString);
47
55
  this.children.forEach(node => allAttachedNodes.push(node.serialize())); // no children is expected under ajax nodes at this time
@@ -47,7 +47,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
47
47
  });
48
48
  this.latestRouteSetByApi = null;
49
49
  this.interactionInProgress = null; // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
50
- this.latestHistoryUrl = null;
50
+ this.latestHistoryUrl = window.location.href; // the initial url is needed to get a correct oldURL in the case that the first nav is triggered by 'popstate'
51
51
  this.harvestOpts.beforeUnload = () => this.interactionInProgress?.done(); // return any withheld ajax or jserr events so they can be sent with EoL harvest
52
52
 
53
53
  this.waitForFlags(['spa']).then(([spaOn]) => {
@@ -63,14 +63,25 @@ class Aggregate extends _aggregateBase.AggregateBase {
63
63
  // By default, a complete UI driven interaction requires event -> URL change -> DOM mod in that exact order.
64
64
  (0, _registerHandler.registerHandler)('newUIEvent', event => this.startUIInteraction(event.type, Math.floor(event.timeStamp), event.target), this.featureName, this.ee);
65
65
  (0, _registerHandler.registerHandler)('newURL', (timestamp, url) => {
66
- // In the case of 'popstate' trigger, by the time the event fires, the URL has already changed, so we need to store what-will-be the *previous* URL for oldURL of next popstate ixn.
66
+ // The newURL always need to be tracked such that it becomes the oldURL of the next potential popstate ixn.
67
+ // Because for 'popstate' triggered newUIEVent, by the time the event fires, the page URL has already changed so the previous URL is lost if not recorded.
67
68
  this.latestHistoryUrl = url;
68
69
  this.interactionInProgress?.updateHistory(timestamp, url);
69
70
  }, this.featureName, this.ee);
70
71
  (0, _registerHandler.registerHandler)('newDom', timestamp => {
71
72
  this.interactionInProgress?.updateDom(timestamp);
72
- if (this.interactionInProgress?.seenHistoryAndDomChange()) this.interactionInProgress.done();
73
+ this.interactionInProgress?.checkHistoryAndDomChange();
73
74
  }, this.featureName, this.ee);
75
+ this.ee.on('long-task', task => {
76
+ if (!this.interactionInProgress?.watchLongtaskTimer) return; // no ixn in progress or it's not yet in a pending-finish state, as indicated by the lack of a watchLongtask timeout
77
+ clearTimeout(this.interactionInProgress.watchLongtaskTimer);
78
+ // Provided there isn't another long task, the ixn span will be extended to include this long task that would finish the interaction.
79
+ this.interactionInProgress.customEnd = task.end;
80
+ this.interactionInProgress.watchLongtaskTimer = setTimeout(() => this.interactionInProgress.done(), _constants.NO_LONG_TASK_WINDOW);
81
+
82
+ // Report metric on frequency of ixn extension due to long task
83
+ this.reportSupportabilityMetric('SoftNav/Interaction/Extended');
84
+ });
74
85
  this.#registerApiHandlers();
75
86
  (0, _registerHandler.registerHandler)('ajax', this.#handleAjaxEvent.bind(this), this.featureName, this.ee);
76
87
  (0, _registerHandler.registerHandler)('jserror', this.#handleJserror.bind(this), this.featureName, this.ee);
@@ -89,9 +100,11 @@ class Aggregate extends _aggregateBase.AggregateBase {
89
100
  startUIInteraction(eventName, startedAt, sourceElem) {
90
101
  // this is throttled by instrumentation so that it isn't excessively called
91
102
  if (this.interactionInProgress?.createdByApi) return; // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
92
- if (this.interactionInProgress?.done() === false) return; // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
103
+ // Navs from interacting with the document will emit the UI event like click, followed by a popstate which should be squashed given some margin of time. This prevents it from cancelling the first UI ixn.
104
+ if (eventName === _constants.POPSTATE_TRIGGER && this.interactionInProgress?.trigger !== _constants.POPSTATE_TRIGGER && startedAt - this.interactionInProgress?.start <= _constants.POPSTATE_MERGE_WINDOW) return;
105
+ if (this.interactionInProgress?.done() === false) return; // current in-progress is blocked from closing if true, e.g. by 'waitForEnd' api option; notice this cancels/finishes existing in-progress ixn
93
106
 
94
- const oldURL = eventName === _constants.INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined; // see related comment in 'newURL' handler above, 'popstate'
107
+ const oldURL = eventName === _constants.POPSTATE_TRIGGER ? this.latestHistoryUrl : undefined; // see related comment in 'newURL' handler above, 'popstate'
95
108
  this.interactionInProgress = new _interaction.Interaction(eventName, startedAt, this.latestRouteSetByApi, oldURL);
96
109
  if (eventName === _constants.INTERACTION_TRIGGERS[0]) {
97
110
  // 'click'
@@ -154,22 +167,30 @@ class Aggregate extends _aggregateBase.AggregateBase {
154
167
  /**
155
168
  * Handles or redirect ajax event based on the interaction, if any, that it's tied to.
156
169
  * @param {Object} event see Ajax feature's storeXhr function for object definition
170
+ * @param {Object} metadata reference to the ajax context, used to pass long task info
157
171
  */
158
- #handleAjaxEvent(event) {
172
+ #handleAjaxEvent(event, metadata) {
159
173
  const associatedInteraction = this.getInteractionFor(event.startTime);
160
174
  if (!associatedInteraction) {
161
175
  // no interaction was happening when this ajax started, so give it back to Ajax feature for processing
162
176
  (0, _handle.handle)('returnAjax', [event], undefined, _features.FEATURE_NAMES.ajax, this.ee);
163
177
  } else {
164
- if (associatedInteraction.status === _constants.INTERACTION_STATUS.FIN) processAjax(event, associatedInteraction); // tack ajax onto the ixn object awaiting harvest
178
+ if (associatedInteraction.status === _constants.INTERACTION_STATUS.FIN) processAjax.call(this, event, metadata, associatedInteraction); // tack ajax onto the ixn object awaiting harvest
165
179
  else {
166
180
  // same thing as above, just at a later time -- if the interaction in progress is cancelled, just send the event back to ajax feat unmodified
167
- associatedInteraction.on('finished', () => processAjax(event, associatedInteraction));
181
+ associatedInteraction.on('finished', () => processAjax.call(this, event, metadata, associatedInteraction));
168
182
  associatedInteraction.on('cancelled', () => (0, _handle.handle)('returnAjax', [event], undefined, _features.FEATURE_NAMES.ajax, this.ee));
169
183
  }
170
184
  }
171
- function processAjax(event, parentInteraction) {
172
- const newNode = new _ajaxNode.AjaxNode(event);
185
+ function processAjax(event, metadata, parentInteraction) {
186
+ const finalEnd = parentInteraction.end; // assume: by the time the 'finished' event occurs & this executes, the ixn end time accounts for any long task extension + lookback window exclusion
187
+ if (event.startTime > finalEnd) {
188
+ (0, _handle.handle)('returnAjax', [event], undefined, _features.FEATURE_NAMES.ajax, this.ee); // falling outside the final span, returned as standalone
189
+ return;
190
+ }
191
+
192
+ // Metadata(ctx) should contain any long task end time associated with this XHR which should be up-to-date by the time the in-progress ixn & ajax children are being finalized for harvest.
193
+ const newNode = new _ajaxNode.AjaxNode(event, metadata);
173
194
  parentInteraction.addChild(newNode);
174
195
  }
175
196
  }
@@ -192,7 +213,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
192
213
  } else {
193
214
  // These callbacks may be added multiple times for an ixn, but just a single run will deal with all jserrors associated with the interaction.
194
215
  // As such, be cautious not to use the params object since that's tied to one specific jserror and won't affect the rest of them.
195
- associatedInteraction.on('finished', (0, _invoke.single)(() => (0, _handle.handle)('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes], undefined, _features.FEATURE_NAMES.jserrors, this.ee)));
216
+ associatedInteraction.on('finished', (0, _invoke.single)(() => (0, _handle.handle)('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes, associatedInteraction.end], undefined, _features.FEATURE_NAMES.jserrors, this.ee)));
196
217
  associatedInteraction.on('cancelled', (0, _invoke.single)(() => (0, _handle.handle)('softNavFlush', [associatedInteraction.id, false, undefined], undefined, _features.FEATURE_NAMES.jserrors, this.ee))); // don't take custom attrs from cancelled ixns
197
218
  }
198
219
  }
@@ -207,7 +228,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
207
228
  this.associatedInteraction = thisClass.getInteractionFor(time);
208
229
  if (this.associatedInteraction?.trigger === _constants.IPL_TRIGGER_NAME) this.associatedInteraction = null; // the api get-interaction method cannot target IPL
209
230
  if (!this.associatedInteraction) {
210
- // This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular seenHistoryAndDomChange process.
231
+ // This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular url>dom change process.
211
232
  this.associatedInteraction = thisClass.interactionInProgress = new _interaction.Interaction(_constants.API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
212
233
  thisClass.domObserver.observe(document.body, {
213
234
  attributes: true,
@@ -217,10 +238,13 @@ class Aggregate extends _aggregateBase.AggregateBase {
217
238
  }); // start observing for DOM changes like a regular UI-driven interaction
218
239
  thisClass.setClosureHandlers();
219
240
  }
220
- if (waitForEnd === true) this.associatedInteraction.keepOpenUntilEndApi = true;
241
+ if (waitForEnd === true) {
242
+ this.associatedInteraction.keepOpenUntilEndApi = true;
243
+ clearTimeout(this.associatedInteraction.cancellationTimer); // get rid of the auto-cancel 30s timer for UI ixns when users specify waitForEnd manual override
244
+ }
221
245
  }, thisClass.featureName, thisClass.ee);
222
246
  (0, _registerHandler.registerHandler)(INTERACTION_API + 'end', function (timeNow) {
223
- this.associatedInteraction.done(timeNow);
247
+ this.associatedInteraction.done(timeNow, true);
224
248
  }, thisClass.featureName, thisClass.ee);
225
249
  (0, _registerHandler.registerHandler)(INTERACTION_API + 'save', function () {
226
250
  this.associatedInteraction.forceSave = true;
@@ -35,7 +35,9 @@ class Interaction extends _belNode.BelNode {
35
35
  createdByApi = false;
36
36
  keepOpenUntilEndApi = false;
37
37
  onDone = [];
38
+ customEnd = 0;
38
39
  cancellationTimer;
40
+ watchLongtaskTimer;
39
41
  constructor(uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
40
42
  super();
41
43
  this.belType = _constants.NODE_TYPE.INTERACTION;
@@ -47,36 +49,48 @@ class Interaction extends _belNode.BelNode {
47
49
  if (this.trigger === _constants.API_TRIGGER_NAME) this.createdByApi = true;
48
50
  this.newURL = this.oldURL = currentUrl || _runtime.globalScope?.location.href;
49
51
  }
50
- updateDom(timestamp) {
51
- this.domTimestamp = timestamp || (0, _now.now)(); // default timestamp should be precise for accurate isActiveDuring calculations
52
- }
53
52
  updateHistory(timestamp, newUrl) {
54
- this.newURL = newUrl || '' + _runtime.globalScope?.location;
53
+ if (this.domTimestamp > 0) return; // url is locked once ui>url>dom change sequence is seen
54
+ if (!newUrl || newUrl === this.oldURL) return; // url must be different for interaction heuristic to proceed
55
+ this.newURL = newUrl;
55
56
  this.historyTimestamp = timestamp || (0, _now.now)();
56
57
  }
57
- seenHistoryAndDomChange() {
58
- return this.historyTimestamp > 0 && this.domTimestamp > this.historyTimestamp; // URL must change before DOM does
58
+ updateDom(timestamp) {
59
+ if (!this.historyTimestamp || timestamp < this.historyTimestamp) return; // dom change must come after (any) url change, though this can be updated multiple times, taking the last dom timestamp
60
+ this.domTimestamp = timestamp || (0, _now.now)(); // default timestamp should be precise for accurate isActiveDuring calculations
61
+ }
62
+ checkHistoryAndDomChange() {
63
+ if (!(this.historyTimestamp > 0 && this.domTimestamp > this.historyTimestamp)) return false;
64
+ if (this.status === _constants.INTERACTION_STATUS.PF) return true; // indicate the finishing process has already started for this interaction
65
+ this.status = _constants.INTERACTION_STATUS.PF; // set for eventual harvest
66
+
67
+ // Once the fixed reqs for a nav has been met, start a X countdown timer that watches for any long task, if it doesn't already exist, before completing the interaction.
68
+ clearTimeout(this.cancellationTimer); // "pending-finish" ixns cannot be auto cancelled anymore
69
+ this.watchLongtaskTimer ??= setTimeout(() => this.done(), _constants.NO_LONG_TASK_WINDOW);
70
+ // Notice that by not providing a specific end time to `.done()`, the ixn will use the dom timestamp in the event of no long task, which is what we want.
71
+ return true;
59
72
  }
60
73
  on(event, cb) {
61
74
  if (!this.eventSubscription.has(event)) throw new Error('Cannot subscribe to non pre-defined events.');
62
75
  if (typeof cb !== 'function') throw new Error('Must supply function as callback.');
63
76
  this.eventSubscription.get(event).push(cb);
64
77
  }
65
- done(customEndTime) {
66
- // User could've mark this interaction--regardless UI or api started--as "don't close until .end() is called on it". Only .end provides a timestamp; the default flows do not.
67
- if (this.keepOpenUntilEndApi && customEndTime === undefined) return false;
78
+ done(customEndTime = this.customEnd, calledByApi = false) {
79
+ // User could've mark this interaction--regardless UI or api started--as "don't close until .end() is called on it".
80
+ if (this.keepOpenUntilEndApi && !calledByApi) return false;
68
81
  // If interaction is already closed, this is a no-op. However, returning true lets startUIInteraction know that it CAN start a new interaction, as this one is done.
69
- if (this.status !== _constants.INTERACTION_STATUS.IP) return true;
82
+ if (this.status === _constants.INTERACTION_STATUS.FIN || this.status === _constants.INTERACTION_STATUS.CAN) return true;
83
+ clearTimeout(this.cancellationTimer); // clean up timers in case this is called by any flow that doesn't already do so
84
+ clearTimeout(this.watchLongtaskTimer);
70
85
  this.onDone.forEach(apiProvidedCb => apiProvidedCb(this.customDataByApi)); // this interaction's .save or .ignore can still be set by these user provided callbacks for example
71
86
 
72
87
  if (this.forceIgnore) this.#cancel(); // .ignore() always has precedence over save actions
73
- else if (this.seenHistoryAndDomChange()) this.#finish(customEndTime); // then this should've already finished while it was the interactionInProgress, with a natural end time
88
+ else if (this.status === _constants.INTERACTION_STATUS.PF) this.#finish(customEndTime); // then this should've already finished while it was the interactionInProgress, with a natural end time
74
89
  else if (this.forceSave) this.#finish(customEndTime || performance.now()); // a manually saved ixn (did not fulfill conditions) must have a specified end time, if one wasn't provided
75
90
  else this.#cancel();
76
91
  return true;
77
92
  }
78
- #finish(customEndTime = 0) {
79
- clearTimeout(this.cancellationTimer);
93
+ #finish(customEndTime) {
80
94
  this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime);
81
95
  this.status = _constants.INTERACTION_STATUS.FIN;
82
96
 
@@ -85,7 +99,6 @@ class Interaction extends _belNode.BelNode {
85
99
  callbacks.forEach(fn => fn());
86
100
  }
87
101
  #cancel() {
88
- clearTimeout(this.cancellationTimer);
89
102
  this.status = _constants.INTERACTION_STATUS.CAN;
90
103
 
91
104
  // Run all the callbacks listening to this interaction's potential cancellation.
@@ -96,12 +109,13 @@ class Interaction extends _belNode.BelNode {
96
109
  /**
97
110
  * Given a timestamp, determine if it falls within this interaction's span, i.e. if this was the active interaction during that time.
98
111
  * For in-progress interactions, this only compares the time with the start of span. Cancelled interactions are not considered active at all.
112
+ * Pending-finish interactions are also considered still active wrt assigning ajax or jserrors to them during the wait period.
99
113
  * @param {DOMHighResTimeStamp} timestamp
100
114
  * @returns True or false boolean.
101
115
  */
102
116
  isActiveDuring(timestamp) {
103
- if (this.status === _constants.INTERACTION_STATUS.IP) return this.start <= timestamp;
104
- return this.status === _constants.INTERACTION_STATUS.FIN && this.start <= timestamp && this.end > timestamp;
117
+ if (this.status === _constants.INTERACTION_STATUS.IP || this.status === _constants.INTERACTION_STATUS.PF) return this.start <= timestamp;
118
+ return this.status === _constants.INTERACTION_STATUS.FIN && this.start <= timestamp && timestamp < this.end;
105
119
  }
106
120
 
107
121
  // Following are virtual properties overridden by a subclass:
@@ -129,10 +143,10 @@ class Interaction extends _belNode.BelNode {
129
143
  // the very 1st ixn does not require offset so it should fallback to a 0 while rest is offset by the very 1st ixn's start
130
144
  (0, _belSerializer.numeric)(this.end - this.start),
131
145
  // end -- relative to start
132
- (0, _belSerializer.numeric)(this.callbackEnd),
133
- // cbEnd -- relative to start; not used by BrowserInteraction events
134
- (0, _belSerializer.numeric)(this.callbackDuration),
135
- // not relative
146
+ (0, _belSerializer.numeric)(0),
147
+ // callbackEnd -- relative to start; not used by BrowserInteraction events so these are always 0
148
+ (0, _belSerializer.numeric)(0),
149
+ // not relative; always 0 for BrowserInteraction
136
150
  addString(this.trigger), addString((0, _cleanUrl.cleanURL)(this.initialPageURL, true)), addString((0, _cleanUrl.cleanURL)(this.oldURL, true)), addString((0, _cleanUrl.cleanURL)(this.newURL, true)), addString(this.customName), ixnType, (0, _belSerializer.nullable)(this.queueTime, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.appTime, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.oldRoute, addString, true) + (0, _belSerializer.nullable)(this.newRoute, addString, true) + addString(this.id), addString(this.nodeId), (0, _belSerializer.nullable)(this.firstPaint, _belSerializer.numeric, true) + (0, _belSerializer.nullable)(this.firstContentfulPaint, _belSerializer.numeric)];
137
151
  const customAttributes = {
138
152
  ...agentRef.info.jsAttributes,
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.NODE_TYPE = exports.IPL_TRIGGER_NAME = exports.INTERACTION_TYPE = exports.INTERACTION_TRIGGERS = exports.INTERACTION_STATUS = exports.FEATURE_NAME = exports.API_TRIGGER_NAME = void 0;
6
+ exports.POPSTATE_TRIGGER = exports.POPSTATE_MERGE_WINDOW = exports.NO_LONG_TASK_WINDOW = exports.NODE_TYPE = exports.IPL_TRIGGER_NAME = exports.INTERACTION_TYPE = exports.INTERACTION_TRIGGERS = exports.INTERACTION_STATUS = exports.FEATURE_NAME = exports.API_TRIGGER_NAME = void 0;
7
7
  var _features = require("../../loaders/features/features");
8
8
  /**
9
9
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
@@ -14,13 +14,15 @@ const INTERACTION_TRIGGERS = exports.INTERACTION_TRIGGERS = ['click',
14
14
  // e.g. user clicks link or the page back/forward buttons
15
15
  'keydown',
16
16
  // e.g. user presses left and right arrow key to switch between displayed photo gallery
17
- 'submit',
18
- // e.g. user clicks submit butotn or presses enter while editing a form field
19
- 'popstate' // history api is used to navigate back and forward
17
+ 'submit' // e.g. user clicks submit butotn or presses enter while editing a form field
20
18
  ];
19
+ const POPSTATE_TRIGGER = exports.POPSTATE_TRIGGER = 'popstate'; // e.g. user clicks browser back/forward button or history API is used programmatically
21
20
  const API_TRIGGER_NAME = exports.API_TRIGGER_NAME = 'api';
22
21
  const IPL_TRIGGER_NAME = exports.IPL_TRIGGER_NAME = 'initialPageLoad';
23
22
  const FEATURE_NAME = exports.FEATURE_NAME = _features.FEATURE_NAMES.softNav;
23
+ const NO_LONG_TASK_WINDOW = exports.NO_LONG_TASK_WINDOW = 5000; // purpose is to wait 5 seconds wherein no long task is detected
24
+ const POPSTATE_MERGE_WINDOW = exports.POPSTATE_MERGE_WINDOW = 500; // "coalesce" (discard) a popstate that happen within this period following an INTERACTION_TRIGGER opening ixn, e.g. click->popstate
25
+
24
26
  const INTERACTION_TYPE = exports.INTERACTION_TYPE = {
25
27
  INITIAL_PAGE_LOAD: '',
26
28
  ROUTE_CHANGE: 1,
@@ -34,6 +36,8 @@ const NODE_TYPE = exports.NODE_TYPE = {
34
36
  };
35
37
  const INTERACTION_STATUS = exports.INTERACTION_STATUS = {
36
38
  IP: 'in progress',
39
+ PF: 'pending finish',
40
+ // interaction meets the hard criteria but is awaiting flexible conditions to fully finish
37
41
  FIN: 'finished',
38
42
  CAN: 'cancelled'
39
43
  };
@@ -36,19 +36,22 @@ class Instrument extends _instrumentBase.InstrumentBase {
36
36
  if (!_runtime.isBrowserScope || !(0, _nreum.gosNREUMOriginals)().o.MO) return; // soft navigations is not supported outside web env or browsers without the mutation observer API
37
37
 
38
38
  const historyEE = (0, _wrapHistory.wrapHistory)(this.ee);
39
+ try {
40
+ this.removeOnAbort = new AbortController();
41
+ } catch (e) {}
39
42
  _constants.INTERACTION_TRIGGERS.forEach(trigger => {
40
43
  (0, _eventListenerOpts.windowAddEventListener)(trigger, evt => {
41
44
  processUserInteraction(evt);
42
- }, true);
45
+ }, true, this.removeOnAbort?.signal);
43
46
  });
44
47
  const trackURLChange = () => (0, _handle.handle)('newURL', [(0, _now.now)(), '' + window.location], undefined, this.featureName, this.ee);
45
48
  historyEE.on('pushState-end', trackURLChange);
46
49
  historyEE.on('replaceState-end', trackURLChange);
47
- try {
48
- this.removeOnAbort = new AbortController();
49
- } catch (e) {}
50
- const trackURLChangeEvent = evt => (0, _handle.handle)('newURL', [evt.timeStamp, '' + window.location], undefined, this.featureName, this.ee);
51
- (0, _eventListenerOpts.windowAddEventListener)('popstate', trackURLChangeEvent, true, this.removeOnAbort?.signal);
50
+ (0, _eventListenerOpts.windowAddEventListener)(_constants.POPSTATE_TRIGGER, evt => {
51
+ // popstate is unique in that it serves as BOTH a UI event and a notification of URL change
52
+ processUserInteraction(evt);
53
+ (0, _handle.handle)('newURL', [evt.timeStamp, '' + window.location], undefined, this.featureName, this.ee);
54
+ }, true, this.removeOnAbort?.signal);
52
55
  let oncePerFrame = false; // attempt to reduce dom noice since the observer runs very frequently with below options
53
56
  const domObserver = new ((0, _nreum.gosNREUMOriginals)().o.MO)((domChanges, observer) => {
54
57
  if (oncePerFrame) return;
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.297.1-rc.0";
14
+ export const VERSION = "1.297.1-rc.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.297.1-rc.0";
14
+ export const VERSION = "1.297.1-rc.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -10,6 +10,7 @@
10
10
  import { ee } from '../event-emitter/contextual-ee';
11
11
  import { bundleId } from '../ids/bundle-id';
12
12
  export const flag = "nr@original:".concat(bundleId);
13
+ const LONG_TASK_THRESHOLD = 50;
13
14
 
14
15
  /**
15
16
  * A convenience alias of `hasOwnProperty`.
@@ -87,7 +88,7 @@ export function createWrapperWithEmitter(emitter, always) {
87
88
  // Warning: start events may mutate args!
88
89
  safeEmit(prefix + 'start', [args, originalThis, methodName], ctx, bubble);
89
90
  const fnStartTime = performance.now();
90
- let fnEndTime = fnStartTime;
91
+ let fnEndTime;
91
92
  try {
92
93
  result = fn.apply(originalThis, args);
93
94
  fnEndTime = performance.now();
@@ -101,16 +102,20 @@ export function createWrapperWithEmitter(emitter, always) {
101
102
  } finally {
102
103
  const duration = fnEndTime - fnStartTime;
103
104
  const task = {
105
+ start: fnStartTime,
106
+ end: fnEndTime,
104
107
  duration,
105
- isLongTask: duration >= 50,
108
+ isLongTask: duration >= LONG_TASK_THRESHOLD,
106
109
  methodName,
107
110
  thrownError
108
111
  // could add more properties here later if needed by downstream features
109
112
  };
110
113
  // standalone long task message
111
- if (task.isLongTask) safeEmit('long-task', [task], ctx, bubble);
114
+ if (task.isLongTask) {
115
+ safeEmit('long-task', [task, originalThis], ctx, bubble);
116
+ }
112
117
  // -end message also includes the task execution info
113
- safeEmit(prefix + 'end', [args, originalThis, result, task], ctx, bubble);
118
+ safeEmit(prefix + 'end', [args, originalThis, result], ctx, bubble);
114
119
  }
115
120
  }
116
121
  }
@@ -11,6 +11,7 @@ import { FEATURE_NAMES } from '../../../loaders/features/features';
11
11
  import { AggregateBase } from '../../utils/aggregate-base';
12
12
  import { parseGQL } from './gql';
13
13
  import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer';
14
+ import { gosNREUMOriginals } from '../../../common/window/nreum';
14
15
  export class Aggregate extends AggregateBase {
15
16
  static featureName = FEATURE_NAME;
16
17
  constructor(agentRef) {
@@ -42,6 +43,13 @@ export class Aggregate extends AggregateBase {
42
43
  // the EE-drain system not only switches "this" but also passes a new EventContext with info. Should consider platform refactor to another system which passes a mutable context around separately and predictably to avoid problems like this.
43
44
  classThis.storeXhr(...arguments, this); // this switches the context back to the class instance while passing the NR context as an argument -- see "ctx" in storeXhr
44
45
  }, this.featureName, this.ee);
46
+ this.ee.on('long-task', (task, originator) => {
47
+ if (originator instanceof gosNREUMOriginals().o.XHR) {
48
+ // any time a long task from XHR callback is observed, update the end time for soft nav use
49
+ const xhrMetadata = this.ee.context(originator);
50
+ xhrMetadata.latestLongtaskEnd = task.end;
51
+ }
52
+ });
45
53
  this.waitForFlags([]).then(() => this.drain());
46
54
  }
47
55
  storeXhr(params, metrics, startTime, endTime, type, ctx) {
@@ -101,8 +109,8 @@ export class Aggregate extends AggregateBase {
101
109
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length);
102
110
  const softNavInUse = Boolean(this.agentRef.features?.[FEATURE_NAMES.softNav]);
103
111
  if (softNavInUse) {
104
- // For newer soft nav (when running), pass the event to it for evaluation -- either part of an interaction or is given back
105
- handle('ajax', [event], undefined, FEATURE_NAMES.softNav, this.ee);
112
+ // For newer soft nav (when running), pass the event w/ info to it for evaluation -- either part of an interaction or is given back
113
+ handle('ajax', [event, ctx], undefined, FEATURE_NAMES.softNav, this.ee);
106
114
  } else if (ctx.spaNode) {
107
115
  // For old spa (when running), if the ajax happened inside an interaction, hold it until the interaction finishes
108
116
  const interactionId = ctx.spaNode.interaction.id;
@@ -98,6 +98,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
98
98
  ctx.loadCaptureCalled = false;
99
99
  ctx.params = this.params || {};
100
100
  ctx.metrics = this.metrics || {};
101
+ ctx.latestLongtaskEnd = 0;
101
102
  xhr.addEventListener('load', function (event) {
102
103
  captureXhrData(ctx, xhr);
103
104
  }, eventListenerOpts(false));
@@ -39,7 +39,7 @@ export class Aggregate extends AggregateBase {
39
39
  this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved));
40
40
  register('err', (...args) => this.storeError(...args), this.featureName, this.ee);
41
41
  register('ierr', (...args) => this.storeError(...args), this.featureName, this.ee);
42
- register('softNavFlush', (interactionId, wasFinished, softNavAttrs) => this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs), this.featureName, this.ee); // when an ixn is done or cancelled
42
+ register('softNavFlush', (interactionId, wasFinished, softNavAttrs, interactionEndTime) => this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs, interactionEndTime), this.featureName, this.ee); // when an ixn is done or cancelled
43
43
 
44
44
  this.harvestOpts.aggregatorTypes = ['err', 'ierr', 'xhr']; // the types in EventAggregator this feature cares about
45
45
 
@@ -266,10 +266,15 @@ export class Aggregate extends AggregateBase {
266
266
  });
267
267
  delete this.bufferedErrorsUnderSpa[interaction.id];
268
268
  }
269
- onSoftNavNotification(interactionId, wasFinished, softNavAttrs) {
269
+ onSoftNavNotification(interactionId, wasFinished, softNavAttrs, interactionEndTime) {
270
270
  if (this.blocked) return;
271
- this.bufferedErrorsUnderSpa[interactionId]?.forEach(jsErrorEvent => this.#storeJserrorForHarvest(jsErrorEvent, wasFinished, softNavAttrs) // this should not modify the re-used softNavAttrs contents
272
- );
271
+ this.bufferedErrorsUnderSpa[interactionId]?.forEach(jsErrorEvent => {
272
+ // this should not modify the re-used softNavAttrs contents
273
+ if (!wasFinished) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs);
274
+ const startTime = jsErrorEvent[3].time; // in storeError fn, the newMetrics obj contains the time passed to & used by SN to seek the ixn
275
+ if (startTime > interactionEndTime) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs); // disassociate any error that ultimately falls outside the final ixn span
276
+ return this.#storeJserrorForHarvest(jsErrorEvent, true, softNavAttrs);
277
+ });
273
278
  delete this.bufferedErrorsUnderSpa[interactionId]; // wipe the list of jserrors so they aren't duplicated by another call to the same id
274
279
  }
275
280
  }