@newrelic/browser-agent 1.271.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 +15 -0
- package/dist/cjs/common/aggregate/aggregator.js +23 -30
- package/dist/cjs/common/aggregate/event-aggregator.js +84 -0
- package/dist/cjs/common/config/init.js +8 -4
- 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 +57 -36
- package/dist/cjs/features/generic_events/instrument/index.js +1 -1
- 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/config/init.js +8 -4
- 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 +57 -36
- package/dist/esm/features/generic_events/instrument/index.js +1 -1
- 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/config/init.d.ts.map +1 -1
- 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/generic_events/instrument/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 +3 -2
- package/src/common/aggregate/aggregator.js +22 -32
- package/src/common/aggregate/event-aggregator.js +76 -0
- package/src/common/config/init.js +6 -2
- 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 +48 -38
- package/src/features/generic_events/instrument/index.js +2 -0
- 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
|
@@ -21,11 +21,11 @@ export class RecorderEvents {
|
|
|
21
21
|
this.#events.add(event);
|
|
22
22
|
}
|
|
23
23
|
get events() {
|
|
24
|
-
return this.#events.
|
|
24
|
+
return this.#events.get();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** A value which increments with every new mutation node reported. Resets after a harvest is sent */
|
|
28
28
|
get payloadBytesEstimation() {
|
|
29
|
-
return this.#events.
|
|
29
|
+
return this.#events.byteSize();
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -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
|
const ERROR_MODE_SECONDS_WINDOW = 30 * 1000; // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
|
|
12
13
|
/** Reserved room for query param attrs */
|
|
@@ -15,9 +16,6 @@ export class Aggregate extends AggregateBase {
|
|
|
15
16
|
static featureName = FEATURE_NAME;
|
|
16
17
|
constructor(agentRef) {
|
|
17
18
|
super(agentRef, FEATURE_NAME);
|
|
18
|
-
|
|
19
|
-
/** A buffer to hold on to harvested traces in the case that a retry must be made later */
|
|
20
|
-
this.sentTrace = null;
|
|
21
19
|
this.harvestTimeSeconds = agentRef.init.session_trace.harvestTimeSeconds || 30;
|
|
22
20
|
/** Tied to the entitlement flag response from BCS. Will short circuit operations of the agg if false */
|
|
23
21
|
this.entitled = undefined;
|
|
@@ -26,7 +24,7 @@ export class Aggregate extends AggregateBase {
|
|
|
26
24
|
/** If the harvest module is harvesting */
|
|
27
25
|
this.harvesting = false;
|
|
28
26
|
/** TraceStorage is the mechanism that holds, normalizes and aggregates ST nodes. It will be accessed and purged when harvests occur */
|
|
29
|
-
this.
|
|
27
|
+
this.events = new TraceStorage(this);
|
|
30
28
|
/** This agg needs information about sampling (sts) and entitlements (st) to make the appropriate decisions on running */
|
|
31
29
|
this.waitForFlags(['sts', 'st']).then(([stMode, stEntitled]) => this.initialize(stMode, stEntitled));
|
|
32
30
|
}
|
|
@@ -55,9 +53,9 @@ export class Aggregate extends AggregateBase {
|
|
|
55
53
|
if (this.sessionId !== sessionState.value || eventType === 'cross-tab' && this.scheduler?.started && sessionState.sessionTraceMode === MODE.OFF) this.abort(2);
|
|
56
54
|
});
|
|
57
55
|
if (typeof PerformanceNavigationTiming !== 'undefined') {
|
|
58
|
-
this.
|
|
56
|
+
this.events.storeTiming(globalScope.performance?.getEntriesByType?.('navigation')[0]);
|
|
59
57
|
} else {
|
|
60
|
-
this.
|
|
58
|
+
this.events.storeTiming(globalScope.performance?.timing, true);
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
|
|
@@ -69,21 +67,21 @@ export class Aggregate extends AggregateBase {
|
|
|
69
67
|
* If it drains later (due to a mode change), data and handlers will instantly drain instead of waiting for the registry. */
|
|
70
68
|
if (this.mode === MODE.OFF) return this.deregisterDrain();
|
|
71
69
|
this.timeKeeper ??= this.agentRef.runtime.timeKeeper;
|
|
72
|
-
this.scheduler = new HarvestScheduler(
|
|
73
|
-
onFinished: this.
|
|
70
|
+
this.scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
|
|
71
|
+
onFinished: result => this.postHarvestCleanup(result.sent && result.retry),
|
|
74
72
|
retryDelay: this.harvestTimeSeconds,
|
|
75
|
-
getPayload: this.
|
|
73
|
+
getPayload: options => this.makeHarvestPayload(options.retry),
|
|
76
74
|
raw: true
|
|
77
75
|
}, this);
|
|
78
76
|
|
|
79
77
|
/** The handlers set up by the Inst file */
|
|
80
|
-
registerHandler('bst', (...args) => this.
|
|
81
|
-
registerHandler('bstResource', (...args) => this.
|
|
82
|
-
registerHandler('bstHist', (...args) => this.
|
|
83
|
-
registerHandler('bstXhrAgg', (...args) => this.
|
|
84
|
-
registerHandler('bstApi', (...args) => this.
|
|
85
|
-
registerHandler('trace-jserror', (...args) => this.
|
|
86
|
-
registerHandler('pvtAdded', (...args) => this.
|
|
78
|
+
registerHandler('bst', (...args) => this.events.storeEvent(...args), this.featureName, this.ee);
|
|
79
|
+
registerHandler('bstResource', (...args) => this.events.storeResources(...args), this.featureName, this.ee);
|
|
80
|
+
registerHandler('bstHist', (...args) => this.events.storeHist(...args), this.featureName, this.ee);
|
|
81
|
+
registerHandler('bstXhrAgg', (...args) => this.events.storeXhrAgg(...args), this.featureName, this.ee);
|
|
82
|
+
registerHandler('bstApi', (...args) => this.events.storeSTN(...args), this.featureName, this.ee);
|
|
83
|
+
registerHandler('trace-jserror', (...args) => this.events.storeErrorAgg(...args), this.featureName, this.ee);
|
|
84
|
+
registerHandler('pvtAdded', (...args) => this.events.processPVT(...args), this.featureName, this.ee);
|
|
87
85
|
|
|
88
86
|
/** Only start actually harvesting if running in full mode at init time */
|
|
89
87
|
if (this.mode === MODE.FULL) this.startHarvesting();else {
|
|
@@ -104,32 +102,38 @@ export class Aggregate extends AggregateBase {
|
|
|
104
102
|
this.scheduler.runHarvest();
|
|
105
103
|
this.scheduler.startTimer(this.harvestTimeSeconds);
|
|
106
104
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
prepareHarvest(options = {}) {
|
|
110
|
-
this.traceStorage.prevStoredEvents.clear(); // release references to past events for GC
|
|
105
|
+
preHarvestChecks() {
|
|
106
|
+
if (this.mode !== MODE.FULL) return; // only allow harvest if running in full mode
|
|
111
107
|
if (!this.timeKeeper?.ready) return; // this should likely never happen, but just to be safe, we should never harvest if we cant correct time
|
|
112
|
-
if (this.
|
|
113
|
-
if (this.sessionId !== this.agentRef.runtime.session
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
earliestTimeStamp,
|
|
118
|
-
latestTimeStamp
|
|
119
|
-
} = this.traceStorage.takeSTNs();
|
|
120
|
-
if (!stns) return; // there are no trace nodes
|
|
121
|
-
if (options.retry) {
|
|
122
|
-
this.sentTrace = stns;
|
|
108
|
+
if (!this.agentRef.runtime.session) return; // session entity is required for trace to run and continue running
|
|
109
|
+
if (this.sessionId !== this.agentRef.runtime.session.state.value || this.ptid !== this.agentRef.runtime.ptid) {
|
|
110
|
+
// If something unexpected happened and we somehow still got to harvesting after a session identifier changed, we should force-exit instead of harvesting:
|
|
111
|
+
this.abort(3);
|
|
112
|
+
return;
|
|
123
113
|
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
serializer({
|
|
117
|
+
stns
|
|
118
|
+
}) {
|
|
119
|
+
if (!stns.length) return; // there are no processed nodes
|
|
120
|
+
this.everHarvested = true;
|
|
121
|
+
return applyFnToProps(stns, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string');
|
|
122
|
+
}
|
|
123
|
+
queryStringsBuilder({
|
|
124
|
+
stns,
|
|
125
|
+
earliestTimeStamp,
|
|
126
|
+
latestTimeStamp
|
|
127
|
+
}) {
|
|
124
128
|
const firstSessionHarvest = !this.agentRef.runtime.session.state.traceHarvestStarted;
|
|
125
129
|
if (firstSessionHarvest) this.agentRef.runtime.session.write({
|
|
126
130
|
traceHarvestStarted: true
|
|
127
131
|
});
|
|
128
|
-
const hasReplay = this.agentRef.runtime.session
|
|
129
|
-
const endUserId = this.agentRef.info
|
|
130
|
-
|
|
132
|
+
const hasReplay = this.agentRef.runtime.session.state.sessionReplayMode === 1;
|
|
133
|
+
const endUserId = this.agentRef.info.jsAttributes['enduser.id'];
|
|
134
|
+
const entityGuid = this.agentRef.runtime.appMetadata.agents?.[0]?.entityGuid;
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
/* The blob consumer expects the following and will reject if not supplied:
|
|
133
137
|
* browser_monitoring_key
|
|
134
138
|
* type
|
|
135
139
|
* app_id
|
|
@@ -138,61 +142,45 @@ export class Aggregate extends AggregateBase {
|
|
|
138
142
|
*
|
|
139
143
|
* For data that does not fit the schema of the above, it should be url-encoded and placed into `attributes`
|
|
140
144
|
*/
|
|
141
|
-
|
|
145
|
+
|
|
142
146
|
return {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
|
|
178
|
-
},
|
|
179
|
-
body: applyFnToProps(stns, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string')
|
|
147
|
+
browser_monitoring_key: this.agentRef.info.licenseKey,
|
|
148
|
+
type: 'BrowserSessionChunk',
|
|
149
|
+
app_id: this.agentRef.info.applicationID,
|
|
150
|
+
protocol_version: '0',
|
|
151
|
+
timestamp: Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
|
|
152
|
+
attributes: encodeObj({
|
|
153
|
+
...(entityGuid && {
|
|
154
|
+
entityGuid
|
|
155
|
+
}),
|
|
156
|
+
harvestId: "".concat(this.agentRef.runtime.session.state.value, "_").concat(this.agentRef.runtime.ptid, "_").concat(this.agentRef.runtime.harvestCount),
|
|
157
|
+
// this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
|
|
158
|
+
// if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
|
|
159
|
+
// trace payload metadata
|
|
160
|
+
'trace.firstTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
|
|
161
|
+
'trace.lastTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(latestTimeStamp)),
|
|
162
|
+
'trace.nodes': stns.length,
|
|
163
|
+
'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
|
|
164
|
+
// other payload metadata
|
|
165
|
+
agentVersion: this.agentRef.runtime.version,
|
|
166
|
+
...(firstSessionHarvest && {
|
|
167
|
+
firstSessionHarvest
|
|
168
|
+
}),
|
|
169
|
+
...(hasReplay && {
|
|
170
|
+
hasReplay
|
|
171
|
+
}),
|
|
172
|
+
ptid: "".concat(this.ptid),
|
|
173
|
+
session: "".concat(this.sessionId),
|
|
174
|
+
// customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
|
|
175
|
+
...(endUserId && {
|
|
176
|
+
'enduser.id': this.obfuscator.obfuscateString(endUserId)
|
|
177
|
+
}),
|
|
178
|
+
currentUrl: this.obfuscator.obfuscateString(cleanURL('' + location))
|
|
179
|
+
// The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
|
|
180
|
+
}, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
|
|
180
181
|
};
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
/** When the harvest scheduler finishes, this callback is executed. It's main purpose is to determine if the payload needs to be retried
|
|
184
|
-
* and if so, it will take all data from the temporary buffer and place it back into the traceStorage module
|
|
185
|
-
*/
|
|
186
|
-
onHarvestFinished(result) {
|
|
187
|
-
if (result.sent && result.retry && this.sentTrace) {
|
|
188
|
-
// merge previous trace back into buffer to retry for next harvest
|
|
189
|
-
Object.entries(this.sentTrace).forEach(([name, listOfSTNodes]) => {
|
|
190
|
-
this.traceStorage.restoreNode(name, listOfSTNodes);
|
|
191
|
-
});
|
|
192
|
-
this.sentTrace = null;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
184
|
/** Switch from "off" or "error" to full mode (if entitled) */
|
|
197
185
|
switchToFull() {
|
|
198
186
|
if (this.mode === MODE.FULL || !this.entitled || this.blocked) return;
|
|
@@ -203,18 +191,19 @@ export class Aggregate extends AggregateBase {
|
|
|
203
191
|
});
|
|
204
192
|
if (prevMode === MODE.OFF || !this.initialized) return this.initialize(this.mode, this.entitled);
|
|
205
193
|
if (this.initialized) {
|
|
206
|
-
this.
|
|
194
|
+
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
|
|
207
195
|
}
|
|
208
196
|
this.startHarvesting();
|
|
209
197
|
}
|
|
210
198
|
|
|
211
199
|
/** Stop running for the remainder of the page lifecycle */
|
|
212
|
-
abort(
|
|
200
|
+
abort() {
|
|
213
201
|
this.blocked = true;
|
|
214
202
|
this.mode = MODE.OFF;
|
|
215
203
|
this.agentRef.runtime.session.write({
|
|
216
204
|
sessionTraceMode: this.mode
|
|
217
205
|
});
|
|
218
206
|
this.scheduler?.stopTimer();
|
|
207
|
+
this.events.clear();
|
|
219
208
|
}
|
|
220
209
|
}
|
|
@@ -35,8 +35,8 @@ export class TraceStorage {
|
|
|
35
35
|
trace = {};
|
|
36
36
|
earliestTimeStamp = Infinity;
|
|
37
37
|
latestTimeStamp = 0;
|
|
38
|
-
tempStorage = [];
|
|
39
38
|
prevStoredEvents = new Set();
|
|
39
|
+
#backupTrace;
|
|
40
40
|
constructor(parent) {
|
|
41
41
|
this.parent = parent;
|
|
42
42
|
}
|
|
@@ -50,9 +50,6 @@ export class TraceStorage {
|
|
|
50
50
|
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
|
|
51
51
|
if (openedSpace === 0) return;
|
|
52
52
|
}
|
|
53
|
-
while (this.tempStorage.length) {
|
|
54
|
-
this.storeSTN(this.tempStorage.shift());
|
|
55
|
-
}
|
|
56
53
|
if (this.trace[stn.n]) this.trace[stn.n].push(stn);else this.trace[stn.n] = [stn];
|
|
57
54
|
if (stn.s < this.earliestTimeStamp) this.earliestTimeStamp = stn.s;
|
|
58
55
|
if (stn.s > this.latestTimeStamp) this.latestTimeStamp = stn.s;
|
|
@@ -99,13 +96,8 @@ export class TraceStorage {
|
|
|
99
96
|
const partitionListByOriginMap = listOfSTNodes.sort((a, b) => a.s - b.s).reduce(reindexByOriginFn, {});
|
|
100
97
|
return Object.values(partitionListByOriginMap).flat(); // join the partitions back into 1-D, now ordered by origin then start time
|
|
101
98
|
}, this);
|
|
102
|
-
if (stns.length === 0) return {};
|
|
103
|
-
this.trace = {};
|
|
104
|
-
this.nodeCount = 0;
|
|
105
99
|
const earliestTimeStamp = this.earliestTimeStamp;
|
|
106
|
-
this.earliestTimeStamp = Infinity;
|
|
107
100
|
const latestTimeStamp = this.latestTimeStamp;
|
|
108
|
-
this.latestTimeStamp = 0;
|
|
109
101
|
return {
|
|
110
102
|
stns,
|
|
111
103
|
earliestTimeStamp,
|
|
@@ -276,9 +268,26 @@ export class TraceStorage {
|
|
|
276
268
|
if (type !== 'xhr') return;
|
|
277
269
|
this.storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, "".concat(params.status, " ").concat(params.method, ": ").concat(params.host).concat(params.pathname), 'ajax'));
|
|
278
270
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
271
|
+
|
|
272
|
+
/* 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. */
|
|
273
|
+
isEmpty() {
|
|
274
|
+
return this.nodeCount === 0;
|
|
275
|
+
}
|
|
276
|
+
save() {
|
|
277
|
+
this.#backupTrace = this.trace;
|
|
278
|
+
}
|
|
279
|
+
get = this.takeSTNs;
|
|
280
|
+
clear() {
|
|
281
|
+
this.trace = {};
|
|
282
|
+
this.nodeCount = 0;
|
|
283
|
+
this.prevStoredEvents.clear(); // release references to past events for GC
|
|
284
|
+
this.earliestTimeStamp = Infinity;
|
|
285
|
+
this.latestTimeStamp = 0;
|
|
286
|
+
}
|
|
287
|
+
reloadSave() {
|
|
288
|
+
Object.values(this.#backupTrace).forEach(stnsArray => stnsArray.forEach(stn => this.storeSTN(stn)));
|
|
289
|
+
}
|
|
290
|
+
clearSave() {
|
|
291
|
+
this.#backupTrace = undefined;
|
|
283
292
|
}
|
|
284
293
|
}
|
|
@@ -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
|
}) {
|
|
19
18
|
super(agentRef, FEATURE_NAME);
|
|
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
|
this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentRef.agentIdentifier);
|
|
24
23
|
timeToFirstByte.subscribe(({
|
|
@@ -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
|
|
@@ -63,26 +62,16 @@ export class Aggregate extends AggregateBase {
|
|
|
63
62
|
registerHandler('ajax', this.#handleAjaxEvent.bind(this), this.featureName, this.ee);
|
|
64
63
|
registerHandler('jserror', this.#handleJserror.bind(this), this.featureName, this.ee);
|
|
65
64
|
}
|
|
66
|
-
|
|
67
|
-
if (!this.interactionsToHarvest.hasData || this.blocked) return;
|
|
65
|
+
serializer(eventBuffer) {
|
|
68
66
|
// 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.
|
|
69
67
|
// 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.
|
|
70
68
|
let firstIxnStartTime = 0; // the very 1st ixn does not require any offsetting
|
|
71
69
|
const serializedIxnList = [];
|
|
72
|
-
for (const interaction of
|
|
70
|
+
for (const interaction of eventBuffer) {
|
|
73
71
|
serializedIxnList.push(interaction.serialize(firstIxnStartTime));
|
|
74
72
|
if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start);
|
|
75
73
|
}
|
|
76
|
-
|
|
77
|
-
if (options.retry) this.interactionsToHarvest.hold();else this.interactionsToHarvest.clear();
|
|
78
|
-
return {
|
|
79
|
-
body: {
|
|
80
|
-
e: payload
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
onHarvestFinished(result) {
|
|
85
|
-
if (result.sent && result.retry && this.interactionsToHarvest.held.hasData) this.interactionsToHarvest.unhold();else this.interactionsToHarvest.held.clear();
|
|
74
|
+
return "bel.7;".concat(serializedIxnList.join(';'));
|
|
86
75
|
}
|
|
87
76
|
startUIInteraction(eventName, startedAt, sourceElem) {
|
|
88
77
|
// this is throttled by instrumentation so that it isn't excessively called
|
|
@@ -130,9 +119,10 @@ export class Aggregate extends AggregateBase {
|
|
|
130
119
|
*/
|
|
131
120
|
if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress;
|
|
132
121
|
let saveIxn;
|
|
133
|
-
|
|
122
|
+
const interactionsBuffer = this.interactionsToHarvest.get();
|
|
123
|
+
for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) {
|
|
134
124
|
// reverse search for the latest completed interaction for efficiency
|
|
135
|
-
const finishedInteraction =
|
|
125
|
+
const finishedInteraction = interactionsBuffer[idx];
|
|
136
126
|
if (finishedInteraction.isActiveDuring(timestamp)) {
|
|
137
127
|
if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction;
|
|
138
128
|
// 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,11 @@ 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
|
+
INTERACTION_TRIGGERS.forEach(trigger => {
|
|
26
|
+
windowAddEventListener(trigger, evt => {
|
|
27
|
+
processUserInteraction(evt);
|
|
28
|
+
}, true);
|
|
29
|
+
});
|
|
27
30
|
const trackURLChange = () => handle('newURL', [now(), '' + window.location], undefined, this.featureName, this.ee);
|
|
28
31
|
historyEE.on('pushState-end', trackURLChange);
|
|
29
32
|
historyEE.on('replaceState-end', trackURLChange);
|
|
@@ -53,13 +56,6 @@ export class Instrument extends InstrumentBase {
|
|
|
53
56
|
}, UI_WAIT_INTERVAL, {
|
|
54
57
|
leading: true
|
|
55
58
|
});
|
|
56
|
-
eventsEE.on('fn-start', ([evt]) => {
|
|
57
|
-
// set up a new user ixn before the callback for the triggering event executes
|
|
58
|
-
if (INTERACTION_TRIGGERS.includes(evt?.type)) {
|
|
59
|
-
processUserInteraction(evt);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
for (let eventType of INTERACTION_TRIGGERS) document.addEventListener(eventType, () => {/* no-op, this ensures the UI events are monitored by our callback above */});
|
|
63
59
|
this.abortHandler = abort;
|
|
64
60
|
this.importAggregator(agentRef, {
|
|
65
61
|
domObserver
|
|
@@ -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
|
const {
|
|
28
27
|
FEATURE_NAME,
|
|
29
28
|
INTERACTION_EVENTS,
|
|
@@ -46,7 +45,7 @@ export class Aggregate extends AggregateBase {
|
|
|
46
45
|
static featureName = FEATURE_NAME;
|
|
47
46
|
constructor(agentRef) {
|
|
48
47
|
super(agentRef, FEATURE_NAME);
|
|
49
|
-
this.state = {
|
|
48
|
+
const state = this.state = {
|
|
50
49
|
initialPageURL: initialLocation,
|
|
51
50
|
lastSeenUrl: initialLocation,
|
|
52
51
|
lastSeenRouteName: null,
|
|
@@ -60,16 +59,12 @@ export class Aggregate extends AggregateBase {
|
|
|
60
59
|
childTime: 0,
|
|
61
60
|
depth: 0,
|
|
62
61
|
harvestTimeSeconds: agentRef.init.spa.harvestTimeSeconds || 10,
|
|
63
|
-
interactionsToHarvest: new EventBuffer(),
|
|
64
62
|
// The below feature flag is used to disable the SPA ajax fix for specific customers, see https://new-relic.atlassian.net/browse/NR-172169
|
|
65
63
|
disableSpaFix: (agentRef.init.feature_flags || []).indexOf('disable-spa-fix') > -1
|
|
66
64
|
};
|
|
65
|
+
this.spaSerializerClass = new Serializer(this);
|
|
66
|
+
const classThis = this;
|
|
67
67
|
let scheduler;
|
|
68
|
-
this.serializer = new Serializer(this);
|
|
69
|
-
const {
|
|
70
|
-
state,
|
|
71
|
-
serializer
|
|
72
|
-
} = this;
|
|
73
68
|
const baseEE = ee.get(agentRef.agentIdentifier); // <-- parent baseEE
|
|
74
69
|
const mutationEE = baseEE.get('mutation');
|
|
75
70
|
const promiseEE = baseEE.get('promise');
|
|
@@ -115,11 +110,11 @@ export class Aggregate extends AggregateBase {
|
|
|
115
110
|
|
|
116
111
|
this.waitForFlags(['spa']).then(([spaFlag]) => {
|
|
117
112
|
if (spaFlag) {
|
|
118
|
-
scheduler = new HarvestScheduler(
|
|
119
|
-
onFinished:
|
|
113
|
+
scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
|
|
114
|
+
onFinished: result => this.postHarvestCleanup(result.sent && result.retry),
|
|
115
|
+
getPayload: options => this.makeHarvestPayload(options.retry),
|
|
120
116
|
retryDelay: state.harvestTimeSeconds
|
|
121
117
|
}, this);
|
|
122
|
-
scheduler.harvest.on('events', onHarvestStarted);
|
|
123
118
|
this.drain();
|
|
124
119
|
} else {
|
|
125
120
|
this.blocked = true;
|
|
@@ -610,20 +605,6 @@ export class Aggregate extends AggregateBase {
|
|
|
610
605
|
});
|
|
611
606
|
setCurrentNode(null);
|
|
612
607
|
}
|
|
613
|
-
const classThis = this;
|
|
614
|
-
function onHarvestStarted(options) {
|
|
615
|
-
if (!state.interactionsToHarvest.hasData || classThis.blocked) return {};
|
|
616
|
-
var payload = serializer.serializeMultiple(state.interactionsToHarvest.buffer, 0, navTiming);
|
|
617
|
-
if (options.retry) state.interactionsToHarvest.hold();else state.interactionsToHarvest.clear();
|
|
618
|
-
return {
|
|
619
|
-
body: {
|
|
620
|
-
e: payload
|
|
621
|
-
}
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
function onHarvestFinished(result) {
|
|
625
|
-
if (result.sent && result.retry && state.interactionsToHarvest.held.hasData) state.interactionsToHarvest.unhold();else state.interactionsToHarvest.held.clear();
|
|
626
|
-
}
|
|
627
608
|
baseEE.on('spa-jserror', function (type, name, params, metrics) {
|
|
628
609
|
if (!state.currentNode) return;
|
|
629
610
|
params._interactionId = state.currentNode.interaction.id;
|
|
@@ -670,7 +651,7 @@ export class Aggregate extends AggregateBase {
|
|
|
670
651
|
interaction.root.attrs.firstContentfulPaint = firstContentfulPaint.current.value;
|
|
671
652
|
}
|
|
672
653
|
baseEE.emit('interactionDone', [interaction, true]);
|
|
673
|
-
|
|
654
|
+
classThis.events.add(interaction);
|
|
674
655
|
let smCategory;
|
|
675
656
|
if (interaction.root?.attrs?.trigger === 'initialPageLoad') smCategory = 'InitialPageLoad';else if (interaction.routeChange) smCategory = 'RouteChange';else smCategory = 'Custom';
|
|
676
657
|
handle(SUPPORTABILITY_METRIC_CHANNEL, ["Spa/Interaction/".concat(smCategory, "/Duration/Ms"), Math.max((interaction.root?.end || 0) - (interaction.root?.start || 0), 0)], undefined, FEATURE_NAMES.metrics, baseEE);
|
|
@@ -678,4 +659,7 @@ export class Aggregate extends AggregateBase {
|
|
|
678
659
|
if (!scheduler) warn(19);
|
|
679
660
|
}
|
|
680
661
|
}
|
|
662
|
+
serializer(eventBuffer) {
|
|
663
|
+
return this.spaSerializerClass.serializeMultiple(eventBuffer, 0, navTiming);
|
|
664
|
+
}
|
|
681
665
|
}
|
|
@@ -5,10 +5,16 @@ 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
|
export class AggregateBase extends FeatureBase {
|
|
9
11
|
constructor(agentRef, featureName) {
|
|
10
12
|
super(agentRef.agentIdentifier, featureName);
|
|
11
13
|
this.agentRef = agentRef;
|
|
14
|
+
// Jserror and Metric features uses a singleton EventAggregator instead of a regular EventBuffer.
|
|
15
|
+
if ([FEATURE_NAMES.jserrors, FEATURE_NAMES.metrics].includes(this.featureName)) this.events = agentRef.sharedAggregator;
|
|
16
|
+
// PVE has no need for eventBuffer, and SessionTrace has its own storage mechanism.
|
|
17
|
+
else if (![FEATURE_NAMES.pageViewEvent, FEATURE_NAMES.sessionTrace].includes(this.featureName)) this.events = new EventBuffer();
|
|
12
18
|
this.checkConfiguration(agentRef);
|
|
13
19
|
this.obfuscator = agentRef.runtime.obfuscator;
|
|
14
20
|
}
|
|
@@ -45,6 +51,37 @@ export class AggregateBase extends FeatureBase {
|
|
|
45
51
|
this.drained = true;
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Return harvest payload. A "serializer" function can be defined on a derived class to format the payload.
|
|
56
|
+
* @param {Boolean} shouldRetryOnFail - harvester flag to backup payload for retry later if harvest request fails; this should be moved to harvester logic
|
|
57
|
+
* @returns final payload, or undefined if there are no pending events
|
|
58
|
+
*/
|
|
59
|
+
makeHarvestPayload(shouldRetryOnFail = false, opts = {}) {
|
|
60
|
+
if (this.events.isEmpty(opts)) return;
|
|
61
|
+
// Other conditions and things to do when preparing harvest that is required.
|
|
62
|
+
if (this.preHarvestChecks && !this.preHarvestChecks()) return;
|
|
63
|
+
if (shouldRetryOnFail) this.events.save(opts);
|
|
64
|
+
const returnedData = this.events.get(opts);
|
|
65
|
+
// A serializer or formatter assists in creating the payload `body` from stored events on harvest when defined by derived feature class.
|
|
66
|
+
const body = this.serializer ? this.serializer(returnedData) : returnedData;
|
|
67
|
+
this.events.clear(opts);
|
|
68
|
+
const payload = {
|
|
69
|
+
body
|
|
70
|
+
};
|
|
71
|
+
// Constructs the payload `qs` for relevant features on harvest.
|
|
72
|
+
if (this.queryStringsBuilder) payload.qs = this.queryStringsBuilder(returnedData);
|
|
73
|
+
return payload;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Cleanup task after a harvest.
|
|
78
|
+
* @param {Boolean} harvestFailed - harvester flag to restore events in main buffer for retry later if request failed
|
|
79
|
+
*/
|
|
80
|
+
postHarvestCleanup(harvestFailed = false, opts = {}) {
|
|
81
|
+
if (harvestFailed) this.events.reloadSave(opts);
|
|
82
|
+
this.events.clearSave(opts);
|
|
83
|
+
}
|
|
84
|
+
|
|
48
85
|
/**
|
|
49
86
|
* Checks for additional `jsAttributes` items to support backward compatibility with implementations of the agent where
|
|
50
87
|
* loader configurations may appear after the loader code is executed.
|