@newrelic/browser-agent 1.297.1-rc.3 → 1.297.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 (49) 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/vitals/largest-contentful-paint.js +1 -4
  4. package/dist/cjs/common/wrap/wrap-function.js +4 -9
  5. package/dist/cjs/features/ajax/aggregate/index.js +2 -10
  6. package/dist/cjs/features/ajax/instrument/index.js +0 -1
  7. package/dist/cjs/features/jserrors/aggregate/index.js +4 -9
  8. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +3 -11
  9. package/dist/cjs/features/soft_navigations/aggregate/index.js +14 -38
  10. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +20 -34
  11. package/dist/cjs/features/soft_navigations/constants.js +4 -8
  12. package/dist/cjs/features/soft_navigations/instrument/index.js +6 -9
  13. package/dist/esm/common/constants/env.cdn.js +1 -1
  14. package/dist/esm/common/constants/env.npm.js +1 -1
  15. package/dist/esm/common/vitals/largest-contentful-paint.js +1 -4
  16. package/dist/esm/common/wrap/wrap-function.js +4 -9
  17. package/dist/esm/features/ajax/aggregate/index.js +2 -10
  18. package/dist/esm/features/ajax/instrument/index.js +0 -1
  19. package/dist/esm/features/jserrors/aggregate/index.js +4 -9
  20. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +3 -11
  21. package/dist/esm/features/soft_navigations/aggregate/index.js +15 -39
  22. package/dist/esm/features/soft_navigations/aggregate/interaction.js +21 -35
  23. package/dist/esm/features/soft_navigations/constants.js +3 -7
  24. package/dist/esm/features/soft_navigations/instrument/index.js +7 -10
  25. package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
  26. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  27. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  28. package/dist/types/features/jserrors/aggregate/index.d.ts +1 -1
  29. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  30. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +1 -2
  31. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  32. package/dist/types/features/soft_navigations/aggregate/index.d.ts +1 -1
  33. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  34. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +3 -6
  35. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  36. package/dist/types/features/soft_navigations/constants.d.ts +0 -4
  37. package/dist/types/features/soft_navigations/constants.d.ts.map +1 -1
  38. package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
  39. package/package.json +2 -2
  40. package/src/common/vitals/largest-contentful-paint.js +1 -2
  41. package/src/common/wrap/wrap-function.js +4 -9
  42. package/src/features/ajax/aggregate/index.js +2 -10
  43. package/src/features/ajax/instrument/index.js +0 -1
  44. package/src/features/jserrors/aggregate/index.js +6 -10
  45. package/src/features/soft_navigations/aggregate/ajax-node.js +4 -8
  46. package/src/features/soft_navigations/aggregate/index.js +15 -39
  47. package/src/features/soft_navigations/aggregate/interaction.js +19 -33
  48. package/src/features/soft_navigations/constants.js +2 -5
  49. package/src/features/soft_navigations/instrument/index.js +8 -9
@@ -11,7 +11,6 @@ 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'
15
14
 
16
15
  export class Aggregate extends AggregateBase {
17
16
  static featureName = FEATURE_NAME
@@ -46,13 +45,6 @@ export class Aggregate extends AggregateBase {
46
45
  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
47
46
  }, this.featureName, this.ee)
48
47
 
49
- this.ee.on('long-task', (task, originator) => {
50
- if (originator instanceof gosNREUMOriginals().o.XHR) { // any time a long task from XHR callback is observed, update the end time for soft nav use
51
- const xhrMetadata = this.ee.context(originator)
52
- xhrMetadata.latestLongtaskEnd = task.end
53
- }
54
- })
55
-
56
48
  this.waitForFlags(([])).then(() => this.drain())
57
49
  }
58
50
 
@@ -121,8 +113,8 @@ export class Aggregate extends AggregateBase {
121
113
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length)
122
114
 
123
115
  const softNavInUse = Boolean(this.agentRef.features?.[FEATURE_NAMES.softNav])
124
- if (softNavInUse) { // For newer soft nav (when running), pass the event w/ info to it for evaluation -- either part of an interaction or is given back
125
- handle('ajax', [event, ctx], undefined, FEATURE_NAMES.softNav, this.ee)
116
+ if (softNavInUse) { // For newer soft nav (when running), pass the event to it for evaluation -- either part of an interaction or is given back
117
+ handle('ajax', [event], undefined, FEATURE_NAMES.softNav, this.ee)
126
118
  } else if (ctx.spaNode) { // For old spa (when running), if the ajax happened inside an interaction, hold it until the interaction finishes
127
119
  const interactionId = ctx.spaNode.interaction.id
128
120
  this.underSpaEvents[interactionId] ??= []
@@ -96,7 +96,6 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
96
96
  ctx.loadCaptureCalled = false
97
97
  ctx.params = this.params || {}
98
98
  ctx.metrics = this.metrics || {}
99
- ctx.latestLongtaskEnd = 0
100
99
 
101
100
  xhr.addEventListener('load', function (event) {
102
101
  captureXhrData(ctx, xhr)
@@ -43,8 +43,8 @@ export class Aggregate extends AggregateBase {
43
43
 
44
44
  register('err', (...args) => this.storeError(...args), this.featureName, this.ee)
45
45
  register('ierr', (...args) => this.storeError(...args), this.featureName, this.ee)
46
- register('softNavFlush', (interactionId, wasFinished, softNavAttrs, interactionEndTime) =>
47
- this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs, interactionEndTime), this.featureName, this.ee) // when an ixn is done or cancelled
46
+ register('softNavFlush', (interactionId, wasFinished, softNavAttrs) =>
47
+ this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs), 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
 
@@ -292,16 +292,12 @@ export class Aggregate extends AggregateBase {
292
292
  delete this.bufferedErrorsUnderSpa[interaction.id]
293
293
  }
294
294
 
295
- onSoftNavNotification (interactionId, wasFinished, softNavAttrs, interactionEndTime) {
295
+ onSoftNavNotification (interactionId, wasFinished, softNavAttrs) {
296
296
  if (this.blocked) return
297
297
 
298
- this.bufferedErrorsUnderSpa[interactionId]?.forEach(jsErrorEvent => { // this should not modify the re-used softNavAttrs contents
299
- if (!wasFinished) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs)
300
-
301
- const startTime = jsErrorEvent[3].time // in storeError fn, the newMetrics obj contains the time passed to & used by SN to seek the ixn
302
- if (startTime > interactionEndTime) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs) // disassociate any error that ultimately falls outside the final ixn span
303
- return this.#storeJserrorForHarvest(jsErrorEvent, true, softNavAttrs)
304
- })
298
+ this.bufferedErrorsUnderSpa[interactionId]?.forEach(jsErrorEvent =>
299
+ this.#storeJserrorForHarvest(jsErrorEvent, wasFinished, softNavAttrs) // this should not modify the re-used softNavAttrs contents
300
+ )
305
301
  delete this.bufferedErrorsUnderSpa[interactionId] // wipe the list of jserrors so they aren't duplicated by another call to the same id
306
302
  }
307
303
  }
@@ -7,7 +7,7 @@ import { NODE_TYPE } from '../constants'
7
7
  import { BelNode } from './bel-node'
8
8
 
9
9
  export class AjaxNode extends BelNode {
10
- constructor (ajaxEvent, ajaxContext) {
10
+ constructor (ajaxEvent) {
11
11
  super()
12
12
  this.belType = NODE_TYPE.AJAX
13
13
  this.method = ajaxEvent.method
@@ -22,12 +22,8 @@ export class AjaxNode extends BelNode {
22
22
  this.spanTimestamp = ajaxEvent.spanTimestamp
23
23
  this.gql = ajaxEvent.gql
24
24
 
25
- this.start = ajaxEvent.startTime
25
+ this.start = ajaxEvent.startTime // 5000 --- 5500 --> 10500
26
26
  this.end = ajaxEvent.endTime
27
- if (ajaxContext?.latestLongtaskEnd) {
28
- this.callbackEnd = Math.max(ajaxContext.latestLongtaskEnd, this.end) // typically lt end if non-zero, but added clamping to end just in case
29
- this.callbackDuration = this.callbackEnd - this.end // callbackDuration is the time from ajax loaded to last long task observed from it
30
- } else this.callbackEnd = this.end // if no long task was observed, callbackEnd is the same as end
31
27
  }
32
28
 
33
29
  serialize (parentStartTimestamp, agentRef) {
@@ -40,8 +36,8 @@ export class AjaxNode extends BelNode {
40
36
  0, // this will be overwritten below with number of attached nodes
41
37
  numeric(this.start - parentStartTimestamp), // start relative to parent start (if part of first node in payload) or first parent start
42
38
  numeric(this.end - this.start), // end is relative to start
43
- numeric(this.callbackEnd - this.end), // callbackEnd is relative to end
44
- numeric(this.callbackDuration), // not relative
39
+ numeric(this.callbackEnd),
40
+ numeric(this.callbackDuration),
45
41
  addString(this.method),
46
42
  numeric(this.status),
47
43
  addString(this.domain),
@@ -8,7 +8,7 @@ import { single } from '../../../common/util/invoke'
8
8
  import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
9
9
  import { FEATURE_NAMES } from '../../../loaders/features/features'
10
10
  import { AggregateBase } from '../../utils/aggregate-base'
11
- import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS, INTERACTION_TRIGGERS, IPL_TRIGGER_NAME, NO_LONG_TASK_WINDOW, POPSTATE_MERGE_WINDOW, POPSTATE_TRIGGER } from '../constants'
11
+ import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS, INTERACTION_TRIGGERS, IPL_TRIGGER_NAME } from '../constants'
12
12
  import { AjaxNode } from './ajax-node'
13
13
  import { InitialPageLoadInteraction } from './initial-page-load-interaction'
14
14
  import { Interaction } from './interaction'
@@ -40,7 +40,7 @@ export class Aggregate extends AggregateBase {
40
40
 
41
41
  this.latestRouteSetByApi = null
42
42
  this.interactionInProgress = null // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
43
- 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'
43
+ this.latestHistoryUrl = null
44
44
  this.harvestOpts.beforeUnload = () => this.interactionInProgress?.done() // return any withheld ajax or jserr events so they can be sent with EoL harvest
45
45
 
46
46
  this.waitForFlags(['spa']).then(([spaOn]) => {
@@ -56,25 +56,14 @@ export class Aggregate extends AggregateBase {
56
56
  // By default, a complete UI driven interaction requires event -> URL change -> DOM mod in that exact order.
57
57
  registerHandler('newUIEvent', (event) => this.startUIInteraction(event.type, Math.floor(event.timeStamp), event.target), this.featureName, this.ee)
58
58
  registerHandler('newURL', (timestamp, url) => {
59
- // The newURL always need to be tracked such that it becomes the oldURL of the next potential popstate ixn.
60
- // 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.
59
+ // 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.
61
60
  this.latestHistoryUrl = url
62
61
  this.interactionInProgress?.updateHistory(timestamp, url)
63
62
  }, this.featureName, this.ee)
64
63
  registerHandler('newDom', timestamp => {
65
64
  this.interactionInProgress?.updateDom(timestamp)
66
- this.interactionInProgress?.checkHistoryAndDomChange()
65
+ if (this.interactionInProgress?.seenHistoryAndDomChange()) this.interactionInProgress.done()
67
66
  }, this.featureName, this.ee)
68
- this.ee.on('long-task', (task) => {
69
- 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
70
- clearTimeout(this.interactionInProgress.watchLongtaskTimer)
71
- // Provided there isn't another long task, the ixn span will be extended to include this long task that would finish the interaction.
72
- this.interactionInProgress.customEnd = task.end
73
- this.interactionInProgress.watchLongtaskTimer = setTimeout(() => this.interactionInProgress.done(), NO_LONG_TASK_WINDOW)
74
-
75
- // Report metric on frequency of ixn extension due to long task
76
- this.reportSupportabilityMetric('SoftNav/Interaction/Extended')
77
- })
78
67
 
79
68
  this.#registerApiHandlers()
80
69
 
@@ -96,11 +85,9 @@ export class Aggregate extends AggregateBase {
96
85
 
97
86
  startUIInteraction (eventName, startedAt, sourceElem) { // this is throttled by instrumentation so that it isn't excessively called
98
87
  if (this.interactionInProgress?.createdByApi) return // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
99
- // 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.
100
- if (eventName === POPSTATE_TRIGGER && this.interactionInProgress?.trigger !== POPSTATE_TRIGGER && startedAt - this.interactionInProgress?.start <= POPSTATE_MERGE_WINDOW) return
101
- 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
88
+ if (this.interactionInProgress?.done() === false) return // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
102
89
 
103
- const oldURL = eventName === POPSTATE_TRIGGER ? this.latestHistoryUrl : undefined // see related comment in 'newURL' handler above, 'popstate'
90
+ const oldURL = eventName === INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined // see related comment in 'newURL' handler above, 'popstate'
104
91
  this.interactionInProgress = new Interaction(eventName, startedAt, this.latestRouteSetByApi, oldURL)
105
92
 
106
93
  if (eventName === INTERACTION_TRIGGERS[0]) { // 'click'
@@ -166,29 +153,21 @@ export class Aggregate extends AggregateBase {
166
153
  /**
167
154
  * Handles or redirect ajax event based on the interaction, if any, that it's tied to.
168
155
  * @param {Object} event see Ajax feature's storeXhr function for object definition
169
- * @param {Object} metadata reference to the ajax context, used to pass long task info
170
156
  */
171
- #handleAjaxEvent (event, metadata) {
157
+ #handleAjaxEvent (event) {
172
158
  const associatedInteraction = this.getInteractionFor(event.startTime)
173
159
  if (!associatedInteraction) { // no interaction was happening when this ajax started, so give it back to Ajax feature for processing
174
160
  handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee)
175
161
  } else {
176
- if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax.call(this, event, metadata, associatedInteraction) // tack ajax onto the ixn object awaiting harvest
162
+ if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax(event, associatedInteraction) // tack ajax onto the ixn object awaiting harvest
177
163
  else { // 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
178
- associatedInteraction.on('finished', () => processAjax.call(this, event, metadata, associatedInteraction))
164
+ associatedInteraction.on('finished', () => processAjax(event, associatedInteraction))
179
165
  associatedInteraction.on('cancelled', () => handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee))
180
166
  }
181
167
  }
182
168
 
183
- function processAjax (event, metadata, parentInteraction) {
184
- 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
185
- if (event.startTime > finalEnd) {
186
- handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee) // falling outside the final span, returned as standalone
187
- return
188
- }
189
-
190
- // 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.
191
- const newNode = new AjaxNode(event, metadata)
169
+ function processAjax (event, parentInteraction) {
170
+ const newNode = new AjaxNode(event)
192
171
  parentInteraction.addChild(newNode)
193
172
  }
194
173
  }
@@ -212,7 +191,7 @@ export class Aggregate extends AggregateBase {
212
191
  // These callbacks may be added multiple times for an ixn, but just a single run will deal with all jserrors associated with the interaction.
213
192
  // 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.
214
193
  associatedInteraction.on('finished', single(() =>
215
- handle('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes, associatedInteraction.end], undefined, FEATURE_NAMES.jserrors, this.ee)))
194
+ handle('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes], undefined, FEATURE_NAMES.jserrors, this.ee)))
216
195
  associatedInteraction.on('cancelled', single(() =>
217
196
  handle('softNavFlush', [associatedInteraction.id, false, undefined], undefined, FEATURE_NAMES.jserrors, this.ee))) // don't take custom attrs from cancelled ixns
218
197
  }
@@ -228,17 +207,14 @@ export class Aggregate extends AggregateBase {
228
207
  this.associatedInteraction = thisClass.getInteractionFor(time)
229
208
  if (this.associatedInteraction?.trigger === IPL_TRIGGER_NAME) this.associatedInteraction = null // the api get-interaction method cannot target IPL
230
209
  if (!this.associatedInteraction) {
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.
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.
232
211
  this.associatedInteraction = thisClass.interactionInProgress = new Interaction(API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi)
233
212
  thisClass.domObserver.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true }) // start observing for DOM changes like a regular UI-driven interaction
234
213
  thisClass.setClosureHandlers()
235
214
  }
236
- if (waitForEnd === true) {
237
- this.associatedInteraction.keepOpenUntilEndApi = true
238
- clearTimeout(this.associatedInteraction.cancellationTimer) // get rid of the auto-cancel 30s timer for UI ixns when users specify waitForEnd manual override
239
- }
215
+ if (waitForEnd === true) this.associatedInteraction.keepOpenUntilEndApi = true
240
216
  }, thisClass.featureName, thisClass.ee)
241
- registerHandler(INTERACTION_API + 'end', function (timeNow) { this.associatedInteraction.done(timeNow, true) }, thisClass.featureName, thisClass.ee)
217
+ registerHandler(INTERACTION_API + 'end', function (timeNow) { this.associatedInteraction.done(timeNow) }, thisClass.featureName, thisClass.ee)
242
218
  registerHandler(INTERACTION_API + 'save', function () { this.associatedInteraction.forceSave = true }, thisClass.featureName, thisClass.ee)
243
219
  registerHandler(INTERACTION_API + 'ignore', function () { this.associatedInteraction.forceIgnore = true }, thisClass.featureName, thisClass.ee)
244
220
 
@@ -7,7 +7,7 @@ import { generateUuid } from '../../../common/ids/unique-id'
7
7
  import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer'
8
8
  import { now } from '../../../common/timing/now'
9
9
  import { cleanURL } from '../../../common/url/clean-url'
10
- import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME, IPL_TRIGGER_NAME, NO_LONG_TASK_WINDOW } from '../constants'
10
+ import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME, IPL_TRIGGER_NAME } from '../constants'
11
11
  import { BelNode } from './bel-node'
12
12
 
13
13
  /**
@@ -29,9 +29,7 @@ export class Interaction extends BelNode {
29
29
  createdByApi = false
30
30
  keepOpenUntilEndApi = false
31
31
  onDone = []
32
- customEnd = 0
33
32
  cancellationTimer
34
- watchLongtaskTimer
35
33
 
36
34
  constructor (uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
37
35
  super()
@@ -48,28 +46,17 @@ export class Interaction extends BelNode {
48
46
  this.newURL = this.oldURL = (currentUrl || globalScope?.location.href)
49
47
  }
50
48
 
51
- updateHistory (timestamp, newUrl) {
52
- if (this.domTimestamp > 0) return // url is locked once ui>url>dom change sequence is seen
53
- if (!newUrl || newUrl === this.oldURL) return // url must be different for interaction heuristic to proceed
54
- this.newURL = newUrl
55
- this.historyTimestamp = (timestamp || now())
56
- }
57
-
58
49
  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
50
  this.domTimestamp = (timestamp || now()) // default timestamp should be precise for accurate isActiveDuring calculations
61
51
  }
62
52
 
63
- checkHistoryAndDomChange () {
64
- if (!(this.historyTimestamp > 0 && this.domTimestamp > this.historyTimestamp)) return false
65
- if (this.status === INTERACTION_STATUS.PF) return true // indicate the finishing process has already started for this interaction
66
- this.status = INTERACTION_STATUS.PF // set for eventual harvest
53
+ updateHistory (timestamp, newUrl) {
54
+ this.newURL = newUrl || '' + globalScope?.location
55
+ this.historyTimestamp = (timestamp || now())
56
+ }
67
57
 
68
- // 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.
69
- clearTimeout(this.cancellationTimer) // "pending-finish" ixns cannot be auto cancelled anymore
70
- this.watchLongtaskTimer ??= setTimeout(() => this.done(), NO_LONG_TASK_WINDOW)
71
- // 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.
72
- return true
58
+ seenHistoryAndDomChange () {
59
+ return this.historyTimestamp > 0 && this.domTimestamp > this.historyTimestamp // URL must change before DOM does
73
60
  }
74
61
 
75
62
  on (event, cb) {
@@ -78,24 +65,23 @@ export class Interaction extends BelNode {
78
65
  this.eventSubscription.get(event).push(cb)
79
66
  }
80
67
 
81
- done (customEndTime = this.customEnd, calledByApi = false) {
82
- // User could've mark this interaction--regardless UI or api started--as "don't close until .end() is called on it".
83
- if (this.keepOpenUntilEndApi && !calledByApi) return false
68
+ done (customEndTime) {
69
+ // 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.
70
+ if (this.keepOpenUntilEndApi && customEndTime === undefined) return false
84
71
  // 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.
85
- if (this.status === INTERACTION_STATUS.FIN || this.status === INTERACTION_STATUS.CAN) return true
72
+ if (this.status !== INTERACTION_STATUS.IP) return true
86
73
 
87
- clearTimeout(this.cancellationTimer) // clean up timers in case this is called by any flow that doesn't already do so
88
- clearTimeout(this.watchLongtaskTimer)
89
74
  this.onDone.forEach(apiProvidedCb => apiProvidedCb(this.customDataByApi)) // this interaction's .save or .ignore can still be set by these user provided callbacks for example
90
75
 
91
76
  if (this.forceIgnore) this.#cancel() // .ignore() always has precedence over save actions
92
- else if (this.status === INTERACTION_STATUS.PF) this.#finish(customEndTime) // then this should've already finished while it was the interactionInProgress, with a natural end time
77
+ else if (this.seenHistoryAndDomChange()) this.#finish(customEndTime) // then this should've already finished while it was the interactionInProgress, with a natural end time
93
78
  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
94
79
  else this.#cancel()
95
80
  return true
96
81
  }
97
82
 
98
- #finish (customEndTime) {
83
+ #finish (customEndTime = 0) {
84
+ clearTimeout(this.cancellationTimer)
99
85
  this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime)
100
86
  this.status = INTERACTION_STATUS.FIN
101
87
 
@@ -105,6 +91,7 @@ export class Interaction extends BelNode {
105
91
  }
106
92
 
107
93
  #cancel () {
94
+ clearTimeout(this.cancellationTimer)
108
95
  this.status = INTERACTION_STATUS.CAN
109
96
 
110
97
  // Run all the callbacks listening to this interaction's potential cancellation.
@@ -115,13 +102,12 @@ export class Interaction extends BelNode {
115
102
  /**
116
103
  * Given a timestamp, determine if it falls within this interaction's span, i.e. if this was the active interaction during that time.
117
104
  * For in-progress interactions, this only compares the time with the start of span. Cancelled interactions are not considered active at all.
118
- * Pending-finish interactions are also considered still active wrt assigning ajax or jserrors to them during the wait period.
119
105
  * @param {DOMHighResTimeStamp} timestamp
120
106
  * @returns True or false boolean.
121
107
  */
122
108
  isActiveDuring (timestamp) {
123
- if (this.status === INTERACTION_STATUS.IP || this.status === INTERACTION_STATUS.PF) return this.start <= timestamp
124
- return (this.status === INTERACTION_STATUS.FIN && this.start <= timestamp && timestamp < this.end)
109
+ if (this.status === INTERACTION_STATUS.IP) return this.start <= timestamp
110
+ return (this.status === INTERACTION_STATUS.FIN && this.start <= timestamp && this.end > timestamp)
125
111
  }
126
112
 
127
113
  // Following are virtual properties overridden by a subclass:
@@ -150,8 +136,8 @@ export class Interaction extends BelNode {
150
136
  0, // this will be overwritten below with number of attached nodes
151
137
  numeric(this.start - (isFirstIxnOfPayload ? 0 : firstStartTimeOfPayload)), // 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
152
138
  numeric(this.end - this.start), // end -- relative to start
153
- numeric(0), // callbackEnd -- relative to start; not used by BrowserInteraction events so these are always 0
154
- numeric(0), // not relative; always 0 for BrowserInteraction
139
+ numeric(this.callbackEnd), // cbEnd -- relative to start; not used by BrowserInteraction events
140
+ numeric(this.callbackDuration), // not relative
155
141
  addString(this.trigger),
156
142
  addString(cleanURL(this.initialPageURL, true)),
157
143
  addString(cleanURL(this.oldURL, true)),
@@ -7,15 +7,13 @@ import { FEATURE_NAMES } from '../../loaders/features/features'
7
7
  export const INTERACTION_TRIGGERS = [
8
8
  'click', // e.g. user clicks link or the page back/forward buttons
9
9
  'keydown', // e.g. user presses left and right arrow key to switch between displayed photo gallery
10
- 'submit' // e.g. user clicks submit butotn or presses enter while editing a form field
10
+ 'submit', // e.g. user clicks submit butotn or presses enter while editing a form field
11
+ 'popstate' // history api is used to navigate back and forward
11
12
  ]
12
- export const POPSTATE_TRIGGER = 'popstate' // e.g. user clicks browser back/forward button or history API is used programmatically
13
13
  export const API_TRIGGER_NAME = 'api'
14
14
  export const IPL_TRIGGER_NAME = 'initialPageLoad'
15
15
 
16
16
  export const FEATURE_NAME = FEATURE_NAMES.softNav
17
- export const NO_LONG_TASK_WINDOW = 5000 // purpose is to wait 5 seconds wherein no long task is detected
18
- export const POPSTATE_MERGE_WINDOW = 500 // "coalesce" (discard) a popstate that happen within this period following an INTERACTION_TRIGGER opening ixn, e.g. click->popstate
19
17
 
20
18
  export const INTERACTION_TYPE = {
21
19
  INITIAL_PAGE_LOAD: '',
@@ -32,7 +30,6 @@ export const NODE_TYPE = {
32
30
 
33
31
  export const INTERACTION_STATUS = {
34
32
  IP: 'in progress',
35
- PF: 'pending finish', // interaction meets the hard criteria but is awaiting flexible conditions to fully finish
36
33
  FIN: 'finished',
37
34
  CAN: 'cancelled'
38
35
  }
@@ -9,7 +9,7 @@ import { windowAddEventListener } from '../../../common/event-listener/event-lis
9
9
  import { debounce } from '../../../common/util/invoke'
10
10
  import { wrapHistory } from '../../../common/wrap/wrap-history'
11
11
  import { InstrumentBase } from '../../utils/instrument-base'
12
- import { FEATURE_NAME, INTERACTION_TRIGGERS, POPSTATE_TRIGGER } from '../constants'
12
+ import { FEATURE_NAME, INTERACTION_TRIGGERS } from '../constants'
13
13
  import { now } from '../../../common/timing/now'
14
14
  import { setupInteractionAPI } from '../../../loaders/api/interaction'
15
15
 
@@ -31,23 +31,22 @@ export class Instrument extends InstrumentBase {
31
31
  if (!isBrowserScope || !gosNREUMOriginals().o.MO) return // soft navigations is not supported outside web env or browsers without the mutation observer API
32
32
 
33
33
  const historyEE = wrapHistory(this.ee)
34
- try {
35
- this.removeOnAbort = new AbortController()
36
- } catch (e) {}
37
34
 
38
35
  INTERACTION_TRIGGERS.forEach((trigger) => {
39
36
  windowAddEventListener(trigger, (evt) => {
40
37
  processUserInteraction(evt)
41
- }, true, this.removeOnAbort?.signal)
38
+ }, true)
42
39
  })
43
40
 
44
41
  const trackURLChange = () => handle('newURL', [now(), '' + window.location], undefined, this.featureName, this.ee)
45
42
  historyEE.on('pushState-end', trackURLChange)
46
43
  historyEE.on('replaceState-end', trackURLChange)
47
- windowAddEventListener(POPSTATE_TRIGGER, (evt) => { // popstate is unique in that it serves as BOTH a UI event and a notification of URL change
48
- processUserInteraction(evt)
49
- handle('newURL', [evt.timeStamp, '' + window.location], undefined, this.featureName, this.ee)
50
- }, true, this.removeOnAbort?.signal)
44
+
45
+ try {
46
+ this.removeOnAbort = new AbortController()
47
+ } catch (e) {}
48
+ const trackURLChangeEvent = (evt) => handle('newURL', [evt.timeStamp, '' + window.location], undefined, this.featureName, this.ee)
49
+ windowAddEventListener('popstate', trackURLChangeEvent, true, this.removeOnAbort?.signal)
51
50
 
52
51
  let oncePerFrame = false // attempt to reduce dom noice since the observer runs very frequently with below options
53
52
  const domObserver = new (gosNREUMOriginals().o).MO((domChanges, observer) => {