@newrelic/browser-agent 1.272.0 → 1.273.0
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/CHANGELOG.md +8 -0
- package/dist/cjs/common/aggregate/aggregator.js +23 -30
- package/dist/cjs/common/aggregate/event-aggregator.js +84 -0
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/harvest/harvest-scheduler.js +1 -1
- package/dist/cjs/common/harvest/harvest.js +1 -5
- package/dist/cjs/common/harvest/types.js +0 -1
- package/dist/cjs/features/ajax/aggregate/index.js +52 -62
- package/dist/cjs/features/generic_events/aggregate/index.js +18 -36
- package/dist/cjs/features/jserrors/aggregate/index.js +23 -69
- package/dist/cjs/features/logging/aggregate/index.js +52 -59
- package/dist/cjs/features/metrics/aggregate/index.js +8 -5
- package/dist/cjs/features/page_view_timing/aggregate/index.js +8 -25
- package/dist/cjs/features/session_replay/aggregate/index.js +11 -10
- package/dist/cjs/features/session_replay/shared/recorder-events.js +2 -2
- package/dist/cjs/features/session_trace/aggregate/index.js +77 -88
- package/dist/cjs/features/session_trace/aggregate/trace/storage.js +22 -13
- package/dist/cjs/features/soft_navigations/aggregate/index.js +10 -20
- package/dist/cjs/features/soft_navigations/instrument/index.js +5 -9
- package/dist/cjs/features/spa/aggregate/index.js +10 -26
- package/dist/cjs/features/utils/aggregate-base.js +37 -0
- package/dist/cjs/features/utils/event-buffer.js +36 -87
- package/dist/cjs/features/utils/instrument-base.js +3 -3
- package/dist/cjs/loaders/features/features.js +13 -1
- package/dist/esm/common/aggregate/aggregator.js +23 -30
- package/dist/esm/common/aggregate/event-aggregator.js +78 -0
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/harvest/harvest-scheduler.js +1 -1
- package/dist/esm/common/harvest/harvest.js +1 -5
- package/dist/esm/common/harvest/types.js +0 -1
- package/dist/esm/features/ajax/aggregate/index.js +53 -62
- package/dist/esm/features/generic_events/aggregate/index.js +18 -36
- package/dist/esm/features/jserrors/aggregate/index.js +24 -70
- package/dist/esm/features/logging/aggregate/index.js +52 -59
- package/dist/esm/features/metrics/aggregate/index.js +8 -5
- package/dist/esm/features/page_view_timing/aggregate/index.js +9 -26
- package/dist/esm/features/session_replay/aggregate/index.js +12 -11
- package/dist/esm/features/session_replay/shared/recorder-events.js +2 -2
- package/dist/esm/features/session_trace/aggregate/index.js +77 -88
- package/dist/esm/features/session_trace/aggregate/trace/storage.js +22 -13
- package/dist/esm/features/soft_navigations/aggregate/index.js +11 -21
- package/dist/esm/features/soft_navigations/instrument/index.js +5 -9
- package/dist/esm/features/spa/aggregate/index.js +11 -27
- package/dist/esm/features/utils/aggregate-base.js +37 -0
- package/dist/esm/features/utils/event-buffer.js +36 -88
- package/dist/esm/features/utils/instrument-base.js +3 -3
- package/dist/esm/loaders/features/features.js +12 -0
- package/dist/types/common/aggregate/aggregator.d.ts +4 -6
- package/dist/types/common/aggregate/aggregator.d.ts.map +1 -1
- package/dist/types/common/aggregate/event-aggregator.d.ts +26 -0
- package/dist/types/common/aggregate/event-aggregator.d.ts.map +1 -0
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/common/harvest/types.d.ts +1 -4
- package/dist/types/common/harvest/types.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts +2 -10
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/index.d.ts +5 -11
- package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +4 -7
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/logging/aggregate/index.d.ts +10 -28
- package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts +1 -9
- package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +3 -4
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +17 -19
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/trace/storage.d.ts +10 -6
- package/dist/types/features/session_trace/aggregate/trace/storage.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/index.d.ts +3 -9
- package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
- package/dist/types/features/spa/aggregate/index.d.ts +2 -3
- package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts +14 -0
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/features/utils/event-buffer.d.ts +19 -56
- package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
- package/dist/types/loaders/features/features.d.ts +3 -0
- package/dist/types/loaders/features/features.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/aggregate/aggregator.js +22 -32
- package/src/common/aggregate/event-aggregator.js +76 -0
- package/src/common/harvest/harvest-scheduler.js +1 -1
- package/src/common/harvest/harvest.js +1 -5
- package/src/common/harvest/types.js +0 -1
- package/src/features/ajax/aggregate/index.js +60 -67
- package/src/features/generic_events/aggregate/index.js +14 -39
- package/src/features/jserrors/aggregate/index.js +21 -77
- package/src/features/logging/aggregate/index.js +46 -60
- package/src/features/metrics/aggregate/index.js +6 -4
- package/src/features/page_view_timing/aggregate/index.js +9 -30
- package/src/features/session_replay/aggregate/index.js +10 -14
- package/src/features/session_replay/shared/recorder-events.js +2 -2
- package/src/features/session_trace/aggregate/index.js +64 -73
- package/src/features/session_trace/aggregate/trace/storage.js +25 -14
- package/src/features/soft_navigations/aggregate/index.js +11 -22
- package/src/features/soft_navigations/instrument/index.js +6 -9
- package/src/features/spa/aggregate/index.js +12 -27
- package/src/features/utils/aggregate-base.js +39 -0
- package/src/features/utils/event-buffer.js +36 -83
- package/src/features/utils/instrument-base.js +3 -3
- package/src/loaders/features/features.js +13 -0
- package/dist/cjs/features/ajax/aggregate/chunk.js +0 -51
- package/dist/esm/features/ajax/aggregate/chunk.js +0 -44
- package/dist/types/features/ajax/aggregate/chunk.d.ts +0 -8
- package/dist/types/features/ajax/aggregate/chunk.d.ts.map +0 -1
- package/src/features/ajax/aggregate/chunk.js +0 -52
|
@@ -7,6 +7,7 @@ import { obj as encodeObj } from '../../../common/url/encode'
|
|
|
7
7
|
import { globalScope } from '../../../common/constants/runtime'
|
|
8
8
|
import { MODE, SESSION_EVENTS } from '../../../common/session/constants'
|
|
9
9
|
import { applyFnToProps } from '../../../common/util/traverse'
|
|
10
|
+
import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
|
|
10
11
|
import { cleanURL } from '../../../common/url/clean-url'
|
|
11
12
|
|
|
12
13
|
const ERROR_MODE_SECONDS_WINDOW = 30 * 1000 // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
|
|
@@ -18,8 +19,6 @@ export class Aggregate extends AggregateBase {
|
|
|
18
19
|
constructor (agentRef) {
|
|
19
20
|
super(agentRef, FEATURE_NAME)
|
|
20
21
|
|
|
21
|
-
/** A buffer to hold on to harvested traces in the case that a retry must be made later */
|
|
22
|
-
this.sentTrace = null
|
|
23
22
|
this.harvestTimeSeconds = agentRef.init.session_trace.harvestTimeSeconds || 30
|
|
24
23
|
/** Tied to the entitlement flag response from BCS. Will short circuit operations of the agg if false */
|
|
25
24
|
this.entitled = undefined
|
|
@@ -28,7 +27,7 @@ export class Aggregate extends AggregateBase {
|
|
|
28
27
|
/** If the harvest module is harvesting */
|
|
29
28
|
this.harvesting = false
|
|
30
29
|
/** TraceStorage is the mechanism that holds, normalizes and aggregates ST nodes. It will be accessed and purged when harvests occur */
|
|
31
|
-
this.
|
|
30
|
+
this.events = new TraceStorage(this)
|
|
32
31
|
/** This agg needs information about sampling (sts) and entitlements (st) to make the appropriate decisions on running */
|
|
33
32
|
this.waitForFlags(['sts', 'st'])
|
|
34
33
|
.then(([stMode, stEntitled]) => this.initialize(stMode, stEntitled))
|
|
@@ -60,9 +59,9 @@ export class Aggregate extends AggregateBase {
|
|
|
60
59
|
})
|
|
61
60
|
|
|
62
61
|
if (typeof PerformanceNavigationTiming !== 'undefined') {
|
|
63
|
-
this.
|
|
62
|
+
this.events.storeTiming(globalScope.performance?.getEntriesByType?.('navigation')[0])
|
|
64
63
|
} else {
|
|
65
|
-
this.
|
|
64
|
+
this.events.storeTiming(globalScope.performance?.timing, true)
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
|
|
@@ -77,21 +76,21 @@ export class Aggregate extends AggregateBase {
|
|
|
77
76
|
|
|
78
77
|
this.timeKeeper ??= this.agentRef.runtime.timeKeeper
|
|
79
78
|
|
|
80
|
-
this.scheduler = new HarvestScheduler(
|
|
81
|
-
onFinished: this.
|
|
79
|
+
this.scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
|
|
80
|
+
onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
|
|
82
81
|
retryDelay: this.harvestTimeSeconds,
|
|
83
|
-
getPayload: this.
|
|
82
|
+
getPayload: (options) => this.makeHarvestPayload(options.retry),
|
|
84
83
|
raw: true
|
|
85
84
|
}, this)
|
|
86
85
|
|
|
87
86
|
/** The handlers set up by the Inst file */
|
|
88
|
-
registerHandler('bst', (...args) => this.
|
|
89
|
-
registerHandler('bstResource', (...args) => this.
|
|
90
|
-
registerHandler('bstHist', (...args) => this.
|
|
91
|
-
registerHandler('bstXhrAgg', (...args) => this.
|
|
92
|
-
registerHandler('bstApi', (...args) => this.
|
|
93
|
-
registerHandler('trace-jserror', (...args) => this.
|
|
94
|
-
registerHandler('pvtAdded', (...args) => this.
|
|
87
|
+
registerHandler('bst', (...args) => this.events.storeEvent(...args), this.featureName, this.ee)
|
|
88
|
+
registerHandler('bstResource', (...args) => this.events.storeResources(...args), this.featureName, this.ee)
|
|
89
|
+
registerHandler('bstHist', (...args) => this.events.storeHist(...args), this.featureName, this.ee)
|
|
90
|
+
registerHandler('bstXhrAgg', (...args) => this.events.storeXhrAgg(...args), this.featureName, this.ee)
|
|
91
|
+
registerHandler('bstApi', (...args) => this.events.storeSTN(...args), this.featureName, this.ee)
|
|
92
|
+
registerHandler('trace-jserror', (...args) => this.events.storeErrorAgg(...args), this.featureName, this.ee)
|
|
93
|
+
registerHandler('pvtAdded', (...args) => this.events.processPVT(...args), this.featureName, this.ee)
|
|
95
94
|
|
|
96
95
|
/** Only start actually harvesting if running in full mode at init time */
|
|
97
96
|
if (this.mode === MODE.FULL) this.startHarvesting()
|
|
@@ -112,28 +111,32 @@ export class Aggregate extends AggregateBase {
|
|
|
112
111
|
this.scheduler.startTimer(this.harvestTimeSeconds)
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.traceStorage.prevStoredEvents.clear() // release references to past events for GC
|
|
114
|
+
preHarvestChecks () {
|
|
115
|
+
if (this.mode !== MODE.FULL) return // only allow harvest if running in full mode
|
|
118
116
|
if (!this.timeKeeper?.ready) return // this should likely never happen, but just to be safe, we should never harvest if we cant correct time
|
|
119
|
-
if (this.
|
|
120
|
-
if (this.sessionId !== this.agentRef.runtime.session
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (options.retry) {
|
|
125
|
-
this.sentTrace = stns
|
|
117
|
+
if (!this.agentRef.runtime.session) return // session entity is required for trace to run and continue running
|
|
118
|
+
if (this.sessionId !== this.agentRef.runtime.session.state.value || this.ptid !== this.agentRef.runtime.ptid) {
|
|
119
|
+
// If something unexpected happened and we somehow still got to harvesting after a session identifier changed, we should force-exit instead of harvesting:
|
|
120
|
+
this.abort(3)
|
|
121
|
+
return
|
|
126
122
|
}
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
serializer ({ stns }) {
|
|
127
|
+
if (!stns.length) return // there are no processed nodes
|
|
128
|
+
this.everHarvested = true
|
|
129
|
+
return applyFnToProps(stns, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string')
|
|
130
|
+
}
|
|
127
131
|
|
|
132
|
+
queryStringsBuilder ({ stns, earliestTimeStamp, latestTimeStamp }) {
|
|
128
133
|
const firstSessionHarvest = !this.agentRef.runtime.session.state.traceHarvestStarted
|
|
129
134
|
if (firstSessionHarvest) this.agentRef.runtime.session.write({ traceHarvestStarted: true })
|
|
135
|
+
const hasReplay = this.agentRef.runtime.session.state.sessionReplayMode === 1
|
|
136
|
+
const endUserId = this.agentRef.info.jsAttributes['enduser.id']
|
|
137
|
+
const entityGuid = this.agentRef.runtime.appMetadata.agents?.[0]?.entityGuid
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
const endUserId = this.agentRef.info?.jsAttributes?.['enduser.id']
|
|
133
|
-
|
|
134
|
-
this.everHarvested = true
|
|
135
|
-
|
|
136
|
-
/** The blob consumer expects the following and will reject if not supplied:
|
|
139
|
+
/* The blob consumer expects the following and will reject if not supplied:
|
|
137
140
|
* browser_monitoring_key
|
|
138
141
|
* type
|
|
139
142
|
* app_id
|
|
@@ -142,47 +145,34 @@ export class Aggregate extends AggregateBase {
|
|
|
142
145
|
*
|
|
143
146
|
* For data that does not fit the schema of the above, it should be url-encoded and placed into `attributes`
|
|
144
147
|
*/
|
|
145
|
-
const agentMetadata = this.agentRef.runtime.appMetadata?.agents?.[0] || {}
|
|
146
|
-
return {
|
|
147
|
-
qs: {
|
|
148
|
-
browser_monitoring_key: this.agentRef.info.licenseKey,
|
|
149
|
-
type: 'BrowserSessionChunk',
|
|
150
|
-
app_id: this.agentRef.info.applicationID,
|
|
151
|
-
protocol_version: '0',
|
|
152
|
-
timestamp: Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
|
|
153
|
-
attributes: encodeObj({
|
|
154
|
-
...(agentMetadata.entityGuid && { entityGuid: agentMetadata.entityGuid }),
|
|
155
|
-
harvestId: `${this.agentRef.runtime.session?.state.value}_${this.agentRef.runtime.ptid}_${this.agentRef.runtime.harvestCount}`,
|
|
156
|
-
// this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
|
|
157
|
-
// if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
|
|
158
|
-
// trace payload metadata
|
|
159
|
-
'trace.firstTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
|
|
160
|
-
'trace.lastTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(latestTimeStamp)),
|
|
161
|
-
'trace.nodes': stns.length,
|
|
162
|
-
'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
|
|
163
|
-
// other payload metadata
|
|
164
|
-
agentVersion: this.agentRef.runtime.version,
|
|
165
|
-
...(firstSessionHarvest && { firstSessionHarvest }),
|
|
166
|
-
...(hasReplay && { hasReplay }),
|
|
167
|
-
ptid: `${this.ptid}`,
|
|
168
|
-
session: `${this.sessionId}`,
|
|
169
|
-
// customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
|
|
170
|
-
...(endUserId && { 'enduser.id': this.obfuscator.obfuscateString(endUserId) }),
|
|
171
|
-
currentUrl: this.obfuscator.obfuscateString(cleanURL('' + location))
|
|
172
|
-
// The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
|
|
173
|
-
}, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
|
|
174
|
-
},
|
|
175
|
-
body: applyFnToProps(stns, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string')
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
148
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
149
|
+
return {
|
|
150
|
+
browser_monitoring_key: this.agentRef.info.licenseKey,
|
|
151
|
+
type: 'BrowserSessionChunk',
|
|
152
|
+
app_id: this.agentRef.info.applicationID,
|
|
153
|
+
protocol_version: '0',
|
|
154
|
+
timestamp: Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
|
|
155
|
+
attributes: encodeObj({
|
|
156
|
+
...(entityGuid && { entityGuid }),
|
|
157
|
+
harvestId: `${this.agentRef.runtime.session.state.value}_${this.agentRef.runtime.ptid}_${this.agentRef.runtime.harvestCount}`,
|
|
158
|
+
// this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
|
|
159
|
+
// if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
|
|
160
|
+
// trace payload metadata
|
|
161
|
+
'trace.firstTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
|
|
162
|
+
'trace.lastTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(latestTimeStamp)),
|
|
163
|
+
'trace.nodes': stns.length,
|
|
164
|
+
'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
|
|
165
|
+
// other payload metadata
|
|
166
|
+
agentVersion: this.agentRef.runtime.version,
|
|
167
|
+
...(firstSessionHarvest && { firstSessionHarvest }),
|
|
168
|
+
...(hasReplay && { hasReplay }),
|
|
169
|
+
ptid: `${this.ptid}`,
|
|
170
|
+
session: `${this.sessionId}`,
|
|
171
|
+
// customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
|
|
172
|
+
...(endUserId && { 'enduser.id': this.obfuscator.obfuscateString(endUserId) }),
|
|
173
|
+
currentUrl: this.obfuscator.obfuscateString(cleanURL('' + location))
|
|
174
|
+
// The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
|
|
175
|
+
}, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
|
|
186
176
|
}
|
|
187
177
|
}
|
|
188
178
|
|
|
@@ -194,16 +184,17 @@ export class Aggregate extends AggregateBase {
|
|
|
194
184
|
this.agentRef.runtime.session.write({ sessionTraceMode: this.mode })
|
|
195
185
|
if (prevMode === MODE.OFF || !this.initialized) return this.initialize(this.mode, this.entitled)
|
|
196
186
|
if (this.initialized) {
|
|
197
|
-
this.
|
|
187
|
+
this.events.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // up until now, Trace would've been just buffering nodes up to max, which needs to be trimmed to last X seconds
|
|
198
188
|
}
|
|
199
189
|
this.startHarvesting()
|
|
200
190
|
}
|
|
201
191
|
|
|
202
192
|
/** Stop running for the remainder of the page lifecycle */
|
|
203
|
-
abort (
|
|
193
|
+
abort () {
|
|
204
194
|
this.blocked = true
|
|
205
195
|
this.mode = MODE.OFF
|
|
206
196
|
this.agentRef.runtime.session.write({ sessionTraceMode: this.mode })
|
|
207
197
|
this.scheduler?.stopTimer()
|
|
198
|
+
this.events.clear()
|
|
208
199
|
}
|
|
209
200
|
}
|
|
@@ -29,8 +29,8 @@ export class TraceStorage {
|
|
|
29
29
|
trace = {}
|
|
30
30
|
earliestTimeStamp = Infinity
|
|
31
31
|
latestTimeStamp = 0
|
|
32
|
-
tempStorage = []
|
|
33
32
|
prevStoredEvents = new Set()
|
|
33
|
+
#backupTrace
|
|
34
34
|
|
|
35
35
|
constructor (parent) {
|
|
36
36
|
this.parent = parent
|
|
@@ -44,9 +44,6 @@ export class TraceStorage {
|
|
|
44
44
|
const openedSpace = this.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // but maybe we could make some space by discarding irrelevant nodes if we're in sessioned Error mode
|
|
45
45
|
if (openedSpace === 0) return
|
|
46
46
|
}
|
|
47
|
-
while (this.tempStorage.length) {
|
|
48
|
-
this.storeSTN(this.tempStorage.shift())
|
|
49
|
-
}
|
|
50
47
|
|
|
51
48
|
if (this.trace[stn.n]) this.trace[stn.n].push(stn)
|
|
52
49
|
else this.trace[stn.n] = [stn]
|
|
@@ -96,15 +93,9 @@ export class TraceStorage {
|
|
|
96
93
|
const partitionListByOriginMap = listOfSTNodes.sort((a, b) => a.s - b.s).reduce(reindexByOriginFn, {})
|
|
97
94
|
return Object.values(partitionListByOriginMap).flat() // join the partitions back into 1-D, now ordered by origin then start time
|
|
98
95
|
}, this)
|
|
99
|
-
if (stns.length === 0) return {}
|
|
100
96
|
|
|
101
|
-
this.trace = {}
|
|
102
|
-
this.nodeCount = 0
|
|
103
97
|
const earliestTimeStamp = this.earliestTimeStamp
|
|
104
|
-
this.earliestTimeStamp = Infinity
|
|
105
98
|
const latestTimeStamp = this.latestTimeStamp
|
|
106
|
-
this.latestTimeStamp = 0
|
|
107
|
-
|
|
108
99
|
return { stns, earliestTimeStamp, latestTimeStamp }
|
|
109
100
|
}
|
|
110
101
|
|
|
@@ -282,10 +273,30 @@ export class TraceStorage {
|
|
|
282
273
|
this.storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, `${params.status} ${params.method}: ${params.host}${params.pathname}`, 'ajax'))
|
|
283
274
|
}
|
|
284
275
|
|
|
285
|
-
|
|
286
|
-
|
|
276
|
+
/* Below are the interface expected & required of whatever storage is used across all features on an individual basis. This allows a common `.events` property on Trace. */
|
|
277
|
+
isEmpty () {
|
|
278
|
+
return this.nodeCount === 0
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
save () {
|
|
282
|
+
this.#backupTrace = this.trace
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
get = this.takeSTNs
|
|
286
|
+
|
|
287
|
+
clear () {
|
|
288
|
+
this.trace = {}
|
|
289
|
+
this.nodeCount = 0
|
|
290
|
+
this.prevStoredEvents.clear() // release references to past events for GC
|
|
291
|
+
this.earliestTimeStamp = Infinity
|
|
292
|
+
this.latestTimeStamp = 0
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
reloadSave () {
|
|
296
|
+
Object.values(this.#backupTrace).forEach(stnsArray => stnsArray.forEach(stn => this.storeSTN(stn)))
|
|
297
|
+
}
|
|
287
298
|
|
|
288
|
-
|
|
289
|
-
this
|
|
299
|
+
clearSave () {
|
|
300
|
+
this.#backupTrace = undefined
|
|
290
301
|
}
|
|
291
302
|
}
|
|
@@ -3,10 +3,9 @@ import { registerHandler } from '../../../common/event-emitter/register-handler'
|
|
|
3
3
|
import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
|
|
4
4
|
import { single } from '../../../common/util/invoke'
|
|
5
5
|
import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
|
|
6
|
-
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
6
|
+
import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
|
|
7
7
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
|
|
8
8
|
import { AggregateBase } from '../../utils/aggregate-base'
|
|
9
|
-
import { EventBuffer } from '../../utils/event-buffer'
|
|
10
9
|
import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS } from '../constants'
|
|
11
10
|
import { AjaxNode } from './ajax-node'
|
|
12
11
|
import { InitialPageLoadInteraction } from './initial-page-load-interaction'
|
|
@@ -18,7 +17,7 @@ export class Aggregate extends AggregateBase {
|
|
|
18
17
|
super(agentRef, FEATURE_NAME)
|
|
19
18
|
|
|
20
19
|
const harvestTimeSeconds = agentRef.init.soft_navigations.harvestTimeSeconds || 10
|
|
21
|
-
this.interactionsToHarvest =
|
|
20
|
+
this.interactionsToHarvest = this.events
|
|
22
21
|
this.domObserver = domObserver
|
|
23
22
|
|
|
24
23
|
this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentRef.agentIdentifier)
|
|
@@ -39,12 +38,12 @@ export class Aggregate extends AggregateBase {
|
|
|
39
38
|
this.waitForFlags(['spa']).then(([spaOn]) => {
|
|
40
39
|
if (spaOn) {
|
|
41
40
|
this.drain()
|
|
42
|
-
const scheduler = new HarvestScheduler(
|
|
43
|
-
onFinished: this.
|
|
41
|
+
const scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
|
|
42
|
+
onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
|
|
43
|
+
getPayload: (options) => this.makeHarvestPayload(options.retry),
|
|
44
44
|
retryDelay: harvestTimeSeconds,
|
|
45
45
|
onUnload: () => this.interactionInProgress?.done() // return any held ajax or jserr events so they can be sent with EoL harvest
|
|
46
46
|
}, this)
|
|
47
|
-
scheduler.harvest.on('events', this.onHarvestStarted.bind(this))
|
|
48
47
|
scheduler.startTimer(harvestTimeSeconds, 0)
|
|
49
48
|
} else {
|
|
50
49
|
this.blocked = true // if rum response determines that customer lacks entitlements for spa endpoint, this feature shouldn't harvest
|
|
@@ -66,27 +65,16 @@ export class Aggregate extends AggregateBase {
|
|
|
66
65
|
registerHandler('jserror', this.#handleJserror.bind(this), this.featureName, this.ee)
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
if (!this.interactionsToHarvest.hasData || this.blocked) return
|
|
68
|
+
serializer (eventBuffer) {
|
|
71
69
|
// The payload depacker takes the first ixn of a payload (if there are multiple ixns) and positively offset the subsequent ixns timestamps by that amount.
|
|
72
70
|
// In order to accurately portray the real start & end times of the 2nd & onward ixns, we hence need to negatively offset their start timestamps with that of the 1st ixn.
|
|
73
71
|
let firstIxnStartTime = 0 // the very 1st ixn does not require any offsetting
|
|
74
72
|
const serializedIxnList = []
|
|
75
|
-
for (const interaction of
|
|
73
|
+
for (const interaction of eventBuffer) {
|
|
76
74
|
serializedIxnList.push(interaction.serialize(firstIxnStartTime))
|
|
77
75
|
if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start)
|
|
78
76
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (options.retry) this.interactionsToHarvest.hold()
|
|
82
|
-
else this.interactionsToHarvest.clear()
|
|
83
|
-
|
|
84
|
-
return { body: { e: payload } }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
onHarvestFinished (result) {
|
|
88
|
-
if (result.sent && result.retry && this.interactionsToHarvest.held.hasData) this.interactionsToHarvest.unhold()
|
|
89
|
-
else this.interactionsToHarvest.held.clear()
|
|
77
|
+
return `bel.7;${serializedIxnList.join(';')}`
|
|
90
78
|
}
|
|
91
79
|
|
|
92
80
|
startUIInteraction (eventName, startedAt, sourceElem) { // this is throttled by instrumentation so that it isn't excessively called
|
|
@@ -139,8 +127,9 @@ export class Aggregate extends AggregateBase {
|
|
|
139
127
|
*/
|
|
140
128
|
if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress
|
|
141
129
|
let saveIxn
|
|
142
|
-
|
|
143
|
-
|
|
130
|
+
const interactionsBuffer = this.interactionsToHarvest.get()
|
|
131
|
+
for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) { // reverse search for the latest completed interaction for efficiency
|
|
132
|
+
const finishedInteraction = interactionsBuffer[idx]
|
|
144
133
|
if (finishedInteraction.isActiveDuring(timestamp)) {
|
|
145
134
|
if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction
|
|
146
135
|
// It's possible that a complete interaction occurs before page is fully loaded, so we need to consider if a route-change ixn may have overlapped this iPL
|
|
@@ -3,7 +3,6 @@ import { isBrowserScope } from '../../../common/constants/runtime'
|
|
|
3
3
|
import { handle } from '../../../common/event-emitter/handle'
|
|
4
4
|
import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
|
|
5
5
|
import { debounce } from '../../../common/util/invoke'
|
|
6
|
-
import { wrapEvents } from '../../../common/wrap/wrap-events'
|
|
7
6
|
import { wrapHistory } from '../../../common/wrap/wrap-history'
|
|
8
7
|
import { InstrumentBase } from '../../utils/instrument-base'
|
|
9
8
|
import { FEATURE_NAME, INTERACTION_TRIGGERS } from '../constants'
|
|
@@ -23,7 +22,12 @@ export class Instrument extends InstrumentBase {
|
|
|
23
22
|
if (!isBrowserScope || !gosNREUMOriginals().o.MO) return // soft navigations is not supported outside web env or browsers without the mutation observer API
|
|
24
23
|
|
|
25
24
|
const historyEE = wrapHistory(this.ee)
|
|
26
|
-
|
|
25
|
+
|
|
26
|
+
INTERACTION_TRIGGERS.forEach((trigger) => {
|
|
27
|
+
windowAddEventListener(trigger, (evt) => {
|
|
28
|
+
processUserInteraction(evt)
|
|
29
|
+
}, true)
|
|
30
|
+
})
|
|
27
31
|
|
|
28
32
|
const trackURLChange = () => handle('newURL', [now(), '' + window.location], undefined, this.featureName, this.ee)
|
|
29
33
|
historyEE.on('pushState-end', trackURLChange)
|
|
@@ -50,13 +54,6 @@ export class Instrument extends InstrumentBase {
|
|
|
50
54
|
domObserver.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true })
|
|
51
55
|
}, UI_WAIT_INTERVAL, { leading: true })
|
|
52
56
|
|
|
53
|
-
eventsEE.on('fn-start', ([evt]) => { // set up a new user ixn before the callback for the triggering event executes
|
|
54
|
-
if (INTERACTION_TRIGGERS.includes(evt?.type)) {
|
|
55
|
-
processUserInteraction(evt)
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
for (let eventType of INTERACTION_TRIGGERS) document.addEventListener(eventType, () => { /* no-op, this ensures the UI events are monitored by our callback above */ })
|
|
59
|
-
|
|
60
57
|
this.abortHandler = abort
|
|
61
58
|
this.importAggregator(agentRef, { domObserver })
|
|
62
59
|
|
|
@@ -14,7 +14,7 @@ import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
|
|
|
14
14
|
import { Serializer } from './serializer'
|
|
15
15
|
import { ee } from '../../../common/event-emitter/contextual-ee'
|
|
16
16
|
import * as CONSTANTS from '../constants'
|
|
17
|
-
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
17
|
+
import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
|
|
18
18
|
import { AggregateBase } from '../../utils/aggregate-base'
|
|
19
19
|
import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
|
|
20
20
|
import { firstPaint } from '../../../common/vitals/first-paint'
|
|
@@ -23,7 +23,6 @@ import { initialLocation, loadedAsDeferredBrowserScript } from '../../../common/
|
|
|
23
23
|
import { handle } from '../../../common/event-emitter/handle'
|
|
24
24
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
|
|
25
25
|
import { warn } from '../../../common/util/console'
|
|
26
|
-
import { EventBuffer } from '../../utils/event-buffer'
|
|
27
26
|
|
|
28
27
|
const {
|
|
29
28
|
FEATURE_NAME, INTERACTION_EVENTS, MAX_TIMER_BUDGET, FN_START, FN_END, CB_START, INTERACTION_API, REMAINING,
|
|
@@ -34,7 +33,7 @@ export class Aggregate extends AggregateBase {
|
|
|
34
33
|
constructor (agentRef) {
|
|
35
34
|
super(agentRef, FEATURE_NAME)
|
|
36
35
|
|
|
37
|
-
this.state = {
|
|
36
|
+
const state = this.state = {
|
|
38
37
|
initialPageURL: initialLocation,
|
|
39
38
|
lastSeenUrl: initialLocation,
|
|
40
39
|
lastSeenRouteName: null,
|
|
@@ -48,15 +47,13 @@ export class Aggregate extends AggregateBase {
|
|
|
48
47
|
childTime: 0,
|
|
49
48
|
depth: 0,
|
|
50
49
|
harvestTimeSeconds: agentRef.init.spa.harvestTimeSeconds || 10,
|
|
51
|
-
interactionsToHarvest: new EventBuffer(),
|
|
52
50
|
// The below feature flag is used to disable the SPA ajax fix for specific customers, see https://new-relic.atlassian.net/browse/NR-172169
|
|
53
51
|
disableSpaFix: (agentRef.init.feature_flags || []).indexOf('disable-spa-fix') > -1
|
|
54
52
|
}
|
|
53
|
+
this.spaSerializerClass = new Serializer(this)
|
|
55
54
|
|
|
55
|
+
const classThis = this
|
|
56
56
|
let scheduler
|
|
57
|
-
this.serializer = new Serializer(this)
|
|
58
|
-
|
|
59
|
-
const { state, serializer } = this
|
|
60
57
|
|
|
61
58
|
const baseEE = ee.get(agentRef.agentIdentifier) // <-- parent baseEE
|
|
62
59
|
const mutationEE = baseEE.get('mutation')
|
|
@@ -103,11 +100,11 @@ export class Aggregate extends AggregateBase {
|
|
|
103
100
|
|
|
104
101
|
this.waitForFlags((['spa'])).then(([spaFlag]) => {
|
|
105
102
|
if (spaFlag) {
|
|
106
|
-
scheduler = new HarvestScheduler(
|
|
107
|
-
onFinished:
|
|
103
|
+
scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
|
|
104
|
+
onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
|
|
105
|
+
getPayload: (options) => this.makeHarvestPayload(options.retry),
|
|
108
106
|
retryDelay: state.harvestTimeSeconds
|
|
109
107
|
}, this)
|
|
110
|
-
scheduler.harvest.on('events', onHarvestStarted)
|
|
111
108
|
this.drain()
|
|
112
109
|
} else {
|
|
113
110
|
this.blocked = true
|
|
@@ -670,22 +667,6 @@ export class Aggregate extends AggregateBase {
|
|
|
670
667
|
setCurrentNode(null)
|
|
671
668
|
}
|
|
672
669
|
|
|
673
|
-
const classThis = this
|
|
674
|
-
function onHarvestStarted (options) {
|
|
675
|
-
if (!state.interactionsToHarvest.hasData || classThis.blocked) return {}
|
|
676
|
-
var payload = serializer.serializeMultiple(state.interactionsToHarvest.buffer, 0, navTiming)
|
|
677
|
-
|
|
678
|
-
if (options.retry) state.interactionsToHarvest.hold()
|
|
679
|
-
else state.interactionsToHarvest.clear()
|
|
680
|
-
|
|
681
|
-
return { body: { e: payload } }
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function onHarvestFinished (result) {
|
|
685
|
-
if (result.sent && result.retry && state.interactionsToHarvest.held.hasData) state.interactionsToHarvest.unhold()
|
|
686
|
-
else state.interactionsToHarvest.held.clear()
|
|
687
|
-
}
|
|
688
|
-
|
|
689
670
|
baseEE.on('spa-jserror', function (type, name, params, metrics) {
|
|
690
671
|
if (!state.currentNode) return
|
|
691
672
|
params._interactionId = state.currentNode.interaction.id
|
|
@@ -736,7 +717,7 @@ export class Aggregate extends AggregateBase {
|
|
|
736
717
|
interaction.root.attrs.firstContentfulPaint = firstContentfulPaint.current.value
|
|
737
718
|
}
|
|
738
719
|
baseEE.emit('interactionDone', [interaction, true])
|
|
739
|
-
|
|
720
|
+
classThis.events.add(interaction)
|
|
740
721
|
|
|
741
722
|
let smCategory
|
|
742
723
|
if (interaction.root?.attrs?.trigger === 'initialPageLoad') smCategory = 'InitialPageLoad'
|
|
@@ -748,4 +729,8 @@ export class Aggregate extends AggregateBase {
|
|
|
748
729
|
if (!scheduler) warn(19)
|
|
749
730
|
}
|
|
750
731
|
}
|
|
732
|
+
|
|
733
|
+
serializer (eventBuffer) {
|
|
734
|
+
return this.spaSerializerClass.serializeMultiple(eventBuffer, 0, navTiming)
|
|
735
|
+
}
|
|
751
736
|
}
|
|
@@ -5,11 +5,17 @@ import { gosCDN } from '../../common/window/nreum'
|
|
|
5
5
|
import { drain } from '../../common/drain/drain'
|
|
6
6
|
import { activatedFeatures } from '../../common/util/feature-flags'
|
|
7
7
|
import { Obfuscator } from '../../common/util/obfuscate'
|
|
8
|
+
import { EventBuffer } from './event-buffer'
|
|
9
|
+
import { FEATURE_NAMES } from '../../loaders/features/features'
|
|
8
10
|
|
|
9
11
|
export class AggregateBase extends FeatureBase {
|
|
10
12
|
constructor (agentRef, featureName) {
|
|
11
13
|
super(agentRef.agentIdentifier, featureName)
|
|
12
14
|
this.agentRef = agentRef
|
|
15
|
+
// Jserror and Metric features uses a singleton EventAggregator instead of a regular EventBuffer.
|
|
16
|
+
if ([FEATURE_NAMES.jserrors, FEATURE_NAMES.metrics].includes(this.featureName)) this.events = agentRef.sharedAggregator
|
|
17
|
+
// PVE has no need for eventBuffer, and SessionTrace has its own storage mechanism.
|
|
18
|
+
else if (![FEATURE_NAMES.pageViewEvent, FEATURE_NAMES.sessionTrace].includes(this.featureName)) this.events = new EventBuffer()
|
|
13
19
|
this.checkConfiguration(agentRef)
|
|
14
20
|
this.obfuscator = agentRef.runtime.obfuscator
|
|
15
21
|
}
|
|
@@ -47,6 +53,39 @@ export class AggregateBase extends FeatureBase {
|
|
|
47
53
|
this.drained = true
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Return harvest payload. A "serializer" function can be defined on a derived class to format the payload.
|
|
58
|
+
* @param {Boolean} shouldRetryOnFail - harvester flag to backup payload for retry later if harvest request fails; this should be moved to harvester logic
|
|
59
|
+
* @returns final payload, or undefined if there are no pending events
|
|
60
|
+
*/
|
|
61
|
+
makeHarvestPayload (shouldRetryOnFail = false, opts = {}) {
|
|
62
|
+
if (this.events.isEmpty(opts)) return
|
|
63
|
+
// Other conditions and things to do when preparing harvest that is required.
|
|
64
|
+
if (this.preHarvestChecks && !this.preHarvestChecks()) return
|
|
65
|
+
|
|
66
|
+
if (shouldRetryOnFail) this.events.save(opts)
|
|
67
|
+
const returnedData = this.events.get(opts)
|
|
68
|
+
// A serializer or formatter assists in creating the payload `body` from stored events on harvest when defined by derived feature class.
|
|
69
|
+
const body = this.serializer ? this.serializer(returnedData) : returnedData
|
|
70
|
+
this.events.clear(opts)
|
|
71
|
+
|
|
72
|
+
const payload = {
|
|
73
|
+
body
|
|
74
|
+
}
|
|
75
|
+
// Constructs the payload `qs` for relevant features on harvest.
|
|
76
|
+
if (this.queryStringsBuilder) payload.qs = this.queryStringsBuilder(returnedData)
|
|
77
|
+
return payload
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cleanup task after a harvest.
|
|
82
|
+
* @param {Boolean} harvestFailed - harvester flag to restore events in main buffer for retry later if request failed
|
|
83
|
+
*/
|
|
84
|
+
postHarvestCleanup (harvestFailed = false, opts = {}) {
|
|
85
|
+
if (harvestFailed) this.events.reloadSave(opts)
|
|
86
|
+
this.events.clearSave(opts)
|
|
87
|
+
}
|
|
88
|
+
|
|
50
89
|
/**
|
|
51
90
|
* Checks for additional `jsAttributes` items to support backward compatibility with implementations of the agent where
|
|
52
91
|
* loader configurations may appear after the loader code is executed.
|