@newrelic/browser-agent 1.297.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 (90) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +3 -0
  3. package/dist/cjs/common/constants/agent-constants.js +3 -2
  4. package/dist/cjs/common/constants/env.cdn.js +1 -1
  5. package/dist/cjs/common/constants/env.npm.js +1 -1
  6. package/dist/cjs/common/harvest/types.js +0 -1
  7. package/dist/cjs/common/wrap/wrap-function.js +9 -4
  8. package/dist/cjs/features/ajax/aggregate/index.js +10 -2
  9. package/dist/cjs/features/ajax/instrument/index.js +1 -0
  10. package/dist/cjs/features/jserrors/aggregate/index.js +9 -4
  11. package/dist/cjs/features/session_replay/aggregate/index.js +18 -21
  12. package/dist/cjs/features/session_replay/constants.js +5 -1
  13. package/dist/cjs/features/session_replay/instrument/index.js +36 -50
  14. package/dist/cjs/features/session_replay/shared/recorder.js +47 -25
  15. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +11 -3
  16. package/dist/cjs/features/soft_navigations/aggregate/index.js +38 -14
  17. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +34 -20
  18. package/dist/cjs/features/soft_navigations/constants.js +8 -4
  19. package/dist/cjs/features/soft_navigations/instrument/index.js +9 -6
  20. package/dist/cjs/features/utils/instrument-base.js +6 -2
  21. package/dist/cjs/loaders/api/interaction-types.js +0 -1
  22. package/dist/cjs/loaders/micro-agent.js +5 -2
  23. package/dist/esm/common/constants/agent-constants.js +2 -1
  24. package/dist/esm/common/constants/env.cdn.js +1 -1
  25. package/dist/esm/common/constants/env.npm.js +1 -1
  26. package/dist/esm/common/harvest/types.js +0 -1
  27. package/dist/esm/common/wrap/wrap-function.js +9 -4
  28. package/dist/esm/features/ajax/aggregate/index.js +10 -2
  29. package/dist/esm/features/ajax/instrument/index.js +1 -0
  30. package/dist/esm/features/jserrors/aggregate/index.js +9 -4
  31. package/dist/esm/features/session_replay/aggregate/index.js +18 -21
  32. package/dist/esm/features/session_replay/constants.js +5 -1
  33. package/dist/esm/features/session_replay/instrument/index.js +36 -50
  34. package/dist/esm/features/session_replay/shared/recorder.js +48 -26
  35. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +11 -3
  36. package/dist/esm/features/soft_navigations/aggregate/index.js +39 -15
  37. package/dist/esm/features/soft_navigations/aggregate/interaction.js +35 -21
  38. package/dist/esm/features/soft_navigations/constants.js +7 -3
  39. package/dist/esm/features/soft_navigations/instrument/index.js +10 -7
  40. package/dist/esm/features/utils/instrument-base.js +6 -2
  41. package/dist/esm/loaders/api/interaction-types.js +0 -1
  42. package/dist/esm/loaders/micro-agent.js +5 -2
  43. package/dist/types/common/constants/agent-constants.d.ts +1 -0
  44. package/dist/types/common/constants/agent-constants.d.ts.map +1 -1
  45. package/dist/types/common/harvest/types.d.ts.map +1 -1
  46. package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
  47. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  48. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  49. package/dist/types/features/jserrors/aggregate/index.d.ts +1 -1
  50. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  51. package/dist/types/features/session_replay/aggregate/index.d.ts +9 -2
  52. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  53. package/dist/types/features/session_replay/constants.d.ts +4 -0
  54. package/dist/types/features/session_replay/instrument/index.d.ts +7 -0
  55. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  56. package/dist/types/features/session_replay/shared/recorder.d.ts +10 -4
  57. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  58. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +2 -1
  59. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  60. package/dist/types/features/soft_navigations/aggregate/index.d.ts +1 -1
  61. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  62. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +6 -3
  63. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  64. package/dist/types/features/soft_navigations/constants.d.ts +4 -0
  65. package/dist/types/features/soft_navigations/constants.d.ts.map +1 -1
  66. package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
  67. package/dist/types/features/utils/instrument-base.d.ts +2 -1
  68. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  69. package/dist/types/loaders/api/interaction-types.d.ts.map +1 -1
  70. package/dist/types/loaders/micro-agent.d.ts +5 -2
  71. package/dist/types/loaders/micro-agent.d.ts.map +1 -1
  72. package/package.json +2 -6
  73. package/src/common/constants/agent-constants.js +1 -0
  74. package/src/common/harvest/types.js +0 -1
  75. package/src/common/wrap/wrap-function.js +9 -4
  76. package/src/features/ajax/aggregate/index.js +10 -2
  77. package/src/features/ajax/instrument/index.js +1 -0
  78. package/src/features/jserrors/aggregate/index.js +10 -6
  79. package/src/features/session_replay/aggregate/index.js +18 -19
  80. package/src/features/session_replay/constants.js +5 -1
  81. package/src/features/session_replay/instrument/index.js +39 -40
  82. package/src/features/session_replay/shared/recorder.js +53 -26
  83. package/src/features/soft_navigations/aggregate/ajax-node.js +8 -4
  84. package/src/features/soft_navigations/aggregate/index.js +39 -15
  85. package/src/features/soft_navigations/aggregate/interaction.js +33 -19
  86. package/src/features/soft_navigations/constants.js +5 -2
  87. package/src/features/soft_navigations/instrument/index.js +9 -8
  88. package/src/features/utils/instrument-base.js +7 -2
  89. package/src/loaders/api/interaction-types.js +0 -1
  90. package/src/loaders/micro-agent.js +5 -2
@@ -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 } from '../constants'
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'
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 = null
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'
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,14 +56,25 @@ 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
- // 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.
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.
60
61
  this.latestHistoryUrl = url
61
62
  this.interactionInProgress?.updateHistory(timestamp, url)
62
63
  }, this.featureName, this.ee)
63
64
  registerHandler('newDom', timestamp => {
64
65
  this.interactionInProgress?.updateDom(timestamp)
65
- if (this.interactionInProgress?.seenHistoryAndDomChange()) this.interactionInProgress.done()
66
+ this.interactionInProgress?.checkHistoryAndDomChange()
66
67
  }, 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
+ })
67
78
 
68
79
  this.#registerApiHandlers()
69
80
 
@@ -85,9 +96,11 @@ export class Aggregate extends AggregateBase {
85
96
 
86
97
  startUIInteraction (eventName, startedAt, sourceElem) { // this is throttled by instrumentation so that it isn't excessively called
87
98
  if (this.interactionInProgress?.createdByApi) return // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
88
- if (this.interactionInProgress?.done() === false) return // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
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
89
102
 
90
- const oldURL = eventName === INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined // see related comment in 'newURL' handler above, 'popstate'
103
+ const oldURL = eventName === POPSTATE_TRIGGER ? this.latestHistoryUrl : undefined // see related comment in 'newURL' handler above, 'popstate'
91
104
  this.interactionInProgress = new Interaction(eventName, startedAt, this.latestRouteSetByApi, oldURL)
92
105
 
93
106
  if (eventName === INTERACTION_TRIGGERS[0]) { // 'click'
@@ -153,21 +166,29 @@ export class Aggregate extends AggregateBase {
153
166
  /**
154
167
  * Handles or redirect ajax event based on the interaction, if any, that it's tied to.
155
168
  * @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
156
170
  */
157
- #handleAjaxEvent (event) {
171
+ #handleAjaxEvent (event, metadata) {
158
172
  const associatedInteraction = this.getInteractionFor(event.startTime)
159
173
  if (!associatedInteraction) { // no interaction was happening when this ajax started, so give it back to Ajax feature for processing
160
174
  handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee)
161
175
  } else {
162
- if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax(event, associatedInteraction) // tack ajax onto the ixn object awaiting harvest
176
+ if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax.call(this, event, metadata, associatedInteraction) // tack ajax onto the ixn object awaiting harvest
163
177
  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
164
- associatedInteraction.on('finished', () => processAjax(event, associatedInteraction))
178
+ associatedInteraction.on('finished', () => processAjax.call(this, event, metadata, associatedInteraction))
165
179
  associatedInteraction.on('cancelled', () => handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee))
166
180
  }
167
181
  }
168
182
 
169
- function processAjax (event, parentInteraction) {
170
- const newNode = new AjaxNode(event)
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)
171
192
  parentInteraction.addChild(newNode)
172
193
  }
173
194
  }
@@ -191,7 +212,7 @@ export class Aggregate extends AggregateBase {
191
212
  // These callbacks may be added multiple times for an ixn, but just a single run will deal with all jserrors associated with the interaction.
192
213
  // 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.
193
214
  associatedInteraction.on('finished', single(() =>
194
- handle('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes], undefined, FEATURE_NAMES.jserrors, this.ee)))
215
+ handle('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes, associatedInteraction.end], undefined, FEATURE_NAMES.jserrors, this.ee)))
195
216
  associatedInteraction.on('cancelled', single(() =>
196
217
  handle('softNavFlush', [associatedInteraction.id, false, undefined], undefined, FEATURE_NAMES.jserrors, this.ee))) // don't take custom attrs from cancelled ixns
197
218
  }
@@ -207,14 +228,17 @@ export class Aggregate extends AggregateBase {
207
228
  this.associatedInteraction = thisClass.getInteractionFor(time)
208
229
  if (this.associatedInteraction?.trigger === 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(API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi)
212
233
  thisClass.domObserver.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true }) // start observing for DOM changes like a regular UI-driven interaction
213
234
  thisClass.setClosureHandlers()
214
235
  }
215
- if (waitForEnd === true) this.associatedInteraction.keepOpenUntilEndApi = true
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
+ }
216
240
  }, thisClass.featureName, thisClass.ee)
217
- registerHandler(INTERACTION_API + 'end', function (timeNow) { this.associatedInteraction.done(timeNow) }, thisClass.featureName, thisClass.ee)
241
+ registerHandler(INTERACTION_API + 'end', function (timeNow) { this.associatedInteraction.done(timeNow, true) }, thisClass.featureName, thisClass.ee)
218
242
  registerHandler(INTERACTION_API + 'save', function () { this.associatedInteraction.forceSave = true }, thisClass.featureName, thisClass.ee)
219
243
  registerHandler(INTERACTION_API + 'ignore', function () { this.associatedInteraction.forceIgnore = true }, thisClass.featureName, thisClass.ee)
220
244
 
@@ -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 } from '../constants'
10
+ import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME, IPL_TRIGGER_NAME, NO_LONG_TASK_WINDOW } from '../constants'
11
11
  import { BelNode } from './bel-node'
12
12
 
13
13
  /**
@@ -29,7 +29,9 @@ export class Interaction extends BelNode {
29
29
  createdByApi = false
30
30
  keepOpenUntilEndApi = false
31
31
  onDone = []
32
+ customEnd = 0
32
33
  cancellationTimer
34
+ watchLongtaskTimer
33
35
 
34
36
  constructor (uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
35
37
  super()
@@ -46,17 +48,28 @@ export class Interaction extends BelNode {
46
48
  this.newURL = this.oldURL = (currentUrl || globalScope?.location.href)
47
49
  }
48
50
 
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
+
49
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
50
60
  this.domTimestamp = (timestamp || now()) // default timestamp should be precise for accurate isActiveDuring calculations
51
61
  }
52
62
 
53
- updateHistory (timestamp, newUrl) {
54
- this.newURL = newUrl || '' + globalScope?.location
55
- this.historyTimestamp = (timestamp || now())
56
- }
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
57
67
 
58
- seenHistoryAndDomChange () {
59
- return this.historyTimestamp > 0 && this.domTimestamp > this.historyTimestamp // URL must change before DOM does
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
60
73
  }
61
74
 
62
75
  on (event, cb) {
@@ -65,23 +78,24 @@ export class Interaction extends BelNode {
65
78
  this.eventSubscription.get(event).push(cb)
66
79
  }
67
80
 
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
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
71
84
  // 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.
72
- if (this.status !== INTERACTION_STATUS.IP) return true
85
+ if (this.status === INTERACTION_STATUS.FIN || this.status === INTERACTION_STATUS.CAN) return true
73
86
 
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)
74
89
  this.onDone.forEach(apiProvidedCb => apiProvidedCb(this.customDataByApi)) // this interaction's .save or .ignore can still be set by these user provided callbacks for example
75
90
 
76
91
  if (this.forceIgnore) this.#cancel() // .ignore() always has precedence over save actions
77
- else if (this.seenHistoryAndDomChange()) this.#finish(customEndTime) // then this should've already finished while it was the interactionInProgress, with a natural end time
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
78
93
  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
79
94
  else this.#cancel()
80
95
  return true
81
96
  }
82
97
 
83
- #finish (customEndTime = 0) {
84
- clearTimeout(this.cancellationTimer)
98
+ #finish (customEndTime) {
85
99
  this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime)
86
100
  this.status = INTERACTION_STATUS.FIN
87
101
 
@@ -91,7 +105,6 @@ export class Interaction extends BelNode {
91
105
  }
92
106
 
93
107
  #cancel () {
94
- clearTimeout(this.cancellationTimer)
95
108
  this.status = INTERACTION_STATUS.CAN
96
109
 
97
110
  // Run all the callbacks listening to this interaction's potential cancellation.
@@ -102,12 +115,13 @@ export class Interaction extends BelNode {
102
115
  /**
103
116
  * Given a timestamp, determine if it falls within this interaction's span, i.e. if this was the active interaction during that time.
104
117
  * 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.
105
119
  * @param {DOMHighResTimeStamp} timestamp
106
120
  * @returns True or false boolean.
107
121
  */
108
122
  isActiveDuring (timestamp) {
109
- if (this.status === INTERACTION_STATUS.IP) return this.start <= timestamp
110
- return (this.status === INTERACTION_STATUS.FIN && this.start <= timestamp && this.end > 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)
111
125
  }
112
126
 
113
127
  // Following are virtual properties overridden by a subclass:
@@ -136,8 +150,8 @@ export class Interaction extends BelNode {
136
150
  0, // this will be overwritten below with number of attached nodes
137
151
  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
138
152
  numeric(this.end - this.start), // end -- relative to start
139
- numeric(this.callbackEnd), // cbEnd -- relative to start; not used by BrowserInteraction events
140
- numeric(this.callbackDuration), // not relative
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
141
155
  addString(this.trigger),
142
156
  addString(cleanURL(this.initialPageURL, true)),
143
157
  addString(cleanURL(this.oldURL, true)),
@@ -7,13 +7,15 @@ 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
11
- 'popstate' // history api is used to navigate back and forward
10
+ 'submit' // e.g. user clicks submit butotn or presses enter while editing a form field
12
11
  ]
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
17
19
 
18
20
  export const INTERACTION_TYPE = {
19
21
  INITIAL_PAGE_LOAD: '',
@@ -30,6 +32,7 @@ export const NODE_TYPE = {
30
32
 
31
33
  export const INTERACTION_STATUS = {
32
34
  IP: 'in progress',
35
+ PF: 'pending finish', // interaction meets the hard criteria but is awaiting flexible conditions to fully finish
33
36
  FIN: 'finished',
34
37
  CAN: 'cancelled'
35
38
  }
@@ -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 } from '../constants'
12
+ import { FEATURE_NAME, INTERACTION_TRIGGERS, POPSTATE_TRIGGER } from '../constants'
13
13
  import { now } from '../../../common/timing/now'
14
14
  import { setupInteractionAPI } from '../../../loaders/api/interaction'
15
15
 
@@ -31,22 +31,23 @@ 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) {}
34
37
 
35
38
  INTERACTION_TRIGGERS.forEach((trigger) => {
36
39
  windowAddEventListener(trigger, (evt) => {
37
40
  processUserInteraction(evt)
38
- }, true)
41
+ }, true, this.removeOnAbort?.signal)
39
42
  })
40
43
 
41
44
  const trackURLChange = () => handle('newURL', [now(), '' + window.location], undefined, this.featureName, this.ee)
42
45
  historyEE.on('pushState-end', trackURLChange)
43
46
  historyEE.on('replaceState-end', trackURLChange)
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)
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)
50
51
 
51
52
  let oncePerFrame = false // attempt to reduce dom noice since the observer runs very frequently with below options
52
53
  const domObserver = new (gosNREUMOriginals().o).MO((domChanges, observer) => {
@@ -18,6 +18,8 @@ import { FEATURE_NAMES } from '../../loaders/features/features'
18
18
  import { hasReplayPrerequisite } from '../session_replay/shared/utils'
19
19
  import { canEnableSessionTracking } from './feature-gates'
20
20
  import { single } from '../../common/util/invoke'
21
+ import { SESSION_ERROR } from '../../common/constants/agent-constants'
22
+ import { handle } from '../../common/event-emitter/handle'
21
23
 
22
24
  /**
23
25
  * Base class for instrumenting a feature.
@@ -32,6 +34,8 @@ export class InstrumentBase extends FeatureBase {
32
34
  constructor (agentRef, featureName) {
33
35
  super(agentRef.agentIdentifier, featureName)
34
36
 
37
+ this.agentRef = agentRef
38
+
35
39
  /** @type {Function | undefined} This should be set by any derived Instrument class if it has things to do when feature fails or is killed. */
36
40
  this.abortHandler = undefined
37
41
 
@@ -74,7 +78,7 @@ export class InstrumentBase extends FeatureBase {
74
78
  * @param {Object} agentRef - reference to the base agent ancestor that this feature belongs to
75
79
  * @param {Function} fetchAggregator - a function that returns a promise that resolves to the aggregate module
76
80
  * @param {Object} [argsObjFromInstrument] - any values or references to pass down to aggregate
77
- * @returns void
81
+ * @returns
78
82
  */
79
83
  importAggregator (agentRef, fetchAggregator, argsObjFromInstrument = {}) {
80
84
  if (this.featAggregate) return
@@ -99,7 +103,7 @@ export class InstrumentBase extends FeatureBase {
99
103
  } catch (e) {
100
104
  warn(20, e)
101
105
  this.ee.emit('internal-error', [e])
102
- if (this.featureName === FEATURE_NAMES.sessionReplay) this.abortHandler?.() // SR should stop recording if session DNE
106
+ handle(SESSION_ERROR, [e], undefined, this.featureName, this.ee)
103
107
  }
104
108
 
105
109
  /**
@@ -140,6 +144,7 @@ export class InstrumentBase extends FeatureBase {
140
144
  * @returns
141
145
  */
142
146
  #shouldImportAgg (featureName, session, agentInit) {
147
+ if (this.blocked) return false
143
148
  switch (featureName) {
144
149
  case FEATURE_NAMES.sessionReplay: // the session manager must be initialized successfully for Replay & Trace features
145
150
  return hasReplayPrerequisite(agentInit) && !!session
@@ -86,5 +86,4 @@
86
86
  * @returns {InteractionInstance} Returns the same interaction object allowing method chaining.
87
87
  */
88
88
 
89
- /* istanbul ignore next */
90
89
  export const unused = {}
@@ -34,10 +34,13 @@ const nonAutoFeatures = [
34
34
  ]
35
35
 
36
36
  /**
37
- * @deprecated This feature has been deprecated and will be removed in a future release. A future product centralizing around a single agent instance will be released as a replacement, at which time this loader will be removed.
38
- * --- A minimal agent class designed to only respond to manual user input. As such, this class does not
37
+ * A minimal agent class designed to only respond to manual user input. As such, this class does not
39
38
  * automatically instrument. Instead, each MicroAgent instance will lazy load the required features and can support loading multiple instances on one page.
40
39
  * Out of the box, it can manually handle and report Page View, Page Action, and Error events.
40
+ *
41
+ * @note This loader strategy is slated to be deprecated and eventually removed in a future product release. For better memory usage, build size impacts, entity management and relationships -- a new strategy focused around using a single centralized browser agent instance is actively being worked on. Reach out by email to browser-agent@newrelic.com for more information or if you would like to participate in a limited preview when the feature is ready for early adoption.
42
+ *
43
+ * @see {@link https://www.npmjs.com/package/@newrelic/browser-agent#deploying-one-or-more-micro-agents-per-page} for more information in the documentation.
41
44
  */
42
45
  export class MicroAgent extends MicroAgentBase {
43
46
  /**