@newrelic/browser-agent 1.297.1-rc.0 → 1.297.1-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/wrap/wrap-function.js +9 -4
- package/dist/cjs/features/ajax/aggregate/index.js +10 -2
- package/dist/cjs/features/ajax/instrument/index.js +1 -0
- package/dist/cjs/features/jserrors/aggregate/index.js +9 -4
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +11 -3
- package/dist/cjs/features/soft_navigations/aggregate/index.js +38 -14
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +34 -20
- package/dist/cjs/features/soft_navigations/constants.js +8 -4
- package/dist/cjs/features/soft_navigations/instrument/index.js +9 -6
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/wrap/wrap-function.js +9 -4
- package/dist/esm/features/ajax/aggregate/index.js +10 -2
- package/dist/esm/features/ajax/instrument/index.js +1 -0
- package/dist/esm/features/jserrors/aggregate/index.js +9 -4
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +11 -3
- package/dist/esm/features/soft_navigations/aggregate/index.js +39 -15
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +35 -21
- package/dist/esm/features/soft_navigations/constants.js +7 -3
- package/dist/esm/features/soft_navigations/instrument/index.js +10 -7
- package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +2 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/index.d.ts +1 -1
- package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +6 -3
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/constants.d.ts +4 -0
- package/dist/types/features/soft_navigations/constants.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/wrap/wrap-function.js +9 -4
- package/src/features/ajax/aggregate/index.js +10 -2
- package/src/features/ajax/instrument/index.js +1 -0
- package/src/features/jserrors/aggregate/index.js +10 -6
- package/src/features/soft_navigations/aggregate/ajax-node.js +8 -4
- package/src/features/soft_navigations/aggregate/index.js +39 -15
- package/src/features/soft_navigations/aggregate/interaction.js +33 -19
- package/src/features/soft_navigations/constants.js +5 -2
- package/src/features/soft_navigations/instrument/index.js +9 -8
|
@@ -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) {
|
|
10
|
+
constructor (ajaxEvent, ajaxContext) {
|
|
11
11
|
super()
|
|
12
12
|
this.belType = NODE_TYPE.AJAX
|
|
13
13
|
this.method = ajaxEvent.method
|
|
@@ -22,8 +22,12 @@ 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
|
|
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
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
serialize (parentStartTimestamp, agentRef) {
|
|
@@ -36,8 +40,8 @@ export class AjaxNode extends BelNode {
|
|
|
36
40
|
0, // this will be overwritten below with number of attached nodes
|
|
37
41
|
numeric(this.start - parentStartTimestamp), // start relative to parent start (if part of first node in payload) or first parent start
|
|
38
42
|
numeric(this.end - this.start), // end is relative to start
|
|
39
|
-
numeric(this.callbackEnd),
|
|
40
|
-
numeric(this.callbackDuration),
|
|
43
|
+
numeric(this.callbackEnd - this.end), // callbackEnd is relative to end
|
|
44
|
+
numeric(this.callbackDuration), // not relative
|
|
41
45
|
addString(this.method),
|
|
42
46
|
numeric(this.status),
|
|
43
47
|
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 } 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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
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
|
-
|
|
59
|
-
|
|
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".
|
|
70
|
-
if (this.keepOpenUntilEndApi &&
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
140
|
-
numeric(
|
|
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'
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
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) => {
|