@newrelic/browser-agent 1.320.0 → 1.320.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/cjs/common/config/runtime.js +1 -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/event-listener/event-listener-opts.js +1 -1
- package/dist/cjs/common/harvest/connector.js +200 -0
- package/dist/cjs/common/harvest/harvester.js +5 -8
- package/dist/cjs/common/harvest/send.js +8 -7
- package/dist/cjs/common/payloads/payloads.js +3 -2
- package/dist/cjs/common/serialize/bel-serializer.js +17 -4
- package/dist/cjs/common/timer/interaction-timer.js +2 -2
- package/dist/cjs/common/unload/eol.js +3 -3
- package/dist/cjs/common/util/console.js +2 -1
- package/dist/cjs/common/util/feature-flags.js +2 -1
- package/dist/cjs/common/v2/mfe-vitals.js +34 -11
- package/dist/cjs/common/window/page-visibility.js +1 -1
- package/dist/cjs/features/ajax/aggregate/index.js +19 -8
- package/dist/cjs/features/page_view_event/aggregate/index.js +2 -6
- package/dist/cjs/features/page_view_event/aggregate_v2/index.js +109 -0
- package/dist/cjs/features/page_view_event/instrument/index.js +6 -1
- package/dist/cjs/features/page_view_timing/aggregate/index.js +33 -14
- package/dist/cjs/features/session_replay/instrument/index.js +9 -0
- package/dist/cjs/features/session_replay/shared/recorder.js +2 -1
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +16 -4
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +10 -3
- package/dist/cjs/features/utils/aggregate-base.js +0 -12
- package/dist/cjs/features/utils/instrument-base.js +72 -29
- package/dist/cjs/loaders/agent.js +2 -1
- package/dist/cjs/loaders/api/consent.js +2 -2
- package/dist/cjs/loaders/features/features.js +2 -1
- package/dist/esm/common/config/runtime.js +1 -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/event-listener/event-listener-opts.js +1 -1
- package/dist/esm/common/harvest/connector.js +193 -0
- package/dist/esm/common/harvest/harvester.js +5 -8
- package/dist/esm/common/harvest/send.js +10 -9
- package/dist/esm/common/payloads/payloads.js +3 -2
- package/dist/esm/common/serialize/bel-serializer.js +17 -4
- package/dist/esm/common/timer/interaction-timer.js +2 -2
- package/dist/esm/common/unload/eol.js +3 -3
- package/dist/esm/common/util/console.js +2 -1
- package/dist/esm/common/util/feature-flags.js +2 -1
- package/dist/esm/common/v2/mfe-vitals.js +34 -11
- package/dist/esm/common/window/page-visibility.js +1 -1
- package/dist/esm/features/ajax/aggregate/index.js +19 -8
- package/dist/esm/features/page_view_event/aggregate/index.js +2 -6
- package/dist/esm/features/page_view_event/aggregate_v2/index.js +100 -0
- package/dist/esm/features/page_view_event/instrument/index.js +6 -1
- package/dist/esm/features/page_view_timing/aggregate/index.js +33 -14
- package/dist/esm/features/session_replay/instrument/index.js +9 -0
- package/dist/esm/features/session_replay/shared/recorder.js +2 -1
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +16 -4
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +10 -3
- package/dist/esm/features/utils/aggregate-base.js +0 -12
- package/dist/esm/features/utils/instrument-base.js +71 -28
- package/dist/esm/loaders/agent.js +2 -1
- package/dist/esm/loaders/api/consent.js +2 -2
- package/dist/esm/loaders/features/features.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/config/runtime.d.ts.map +1 -1
- package/dist/types/common/event-listener/event-listener-opts.d.ts +1 -1
- package/dist/types/common/harvest/connector.d.ts +6 -0
- package/dist/types/common/harvest/connector.d.ts.map +1 -0
- package/dist/types/common/harvest/harvester.d.ts.map +1 -1
- package/dist/types/common/harvest/send.d.ts.map +1 -1
- package/dist/types/common/payloads/payloads.d.ts +3 -2
- package/dist/types/common/payloads/payloads.d.ts.map +1 -1
- package/dist/types/common/serialize/bel-serializer.d.ts +9 -1
- package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
- package/dist/types/common/unload/eol.d.ts +1 -1
- package/dist/types/common/unload/eol.d.ts.map +1 -1
- package/dist/types/common/util/console.d.ts +2 -1
- package/dist/types/common/util/console.d.ts.map +1 -1
- package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate_v2/index.d.ts +20 -0
- package/dist/types/features/page_view_event/aggregate_v2/index.d.ts.map +1 -0
- package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts +0 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts +0 -5
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
- package/dist/types/loaders/agent.d.ts.map +1 -1
- package/dist/types/loaders/features/features.d.ts +1 -0
- package/dist/types/loaders/features/features.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/config/runtime.js +1 -0
- package/src/common/event-listener/event-listener-opts.js +1 -1
- package/src/common/harvest/connector.js +168 -0
- package/src/common/harvest/harvester.js +7 -9
- package/src/common/harvest/send.js +9 -7
- package/src/common/payloads/payloads.js +3 -2
- package/src/common/serialize/bel-serializer.js +13 -4
- package/src/common/timer/interaction-timer.js +2 -2
- package/src/common/unload/eol.js +3 -3
- package/src/common/util/console.js +2 -1
- package/src/common/util/feature-flags.js +1 -1
- package/src/common/v2/mfe-vitals.js +39 -12
- package/src/common/window/page-visibility.js +1 -1
- package/src/features/ajax/aggregate/index.js +12 -9
- package/src/features/page_view_event/aggregate/index.js +2 -6
- package/src/features/page_view_event/aggregate_v2/index.js +97 -0
- package/src/features/page_view_event/instrument/index.js +6 -1
- package/src/features/page_view_timing/aggregate/index.js +24 -15
- package/src/features/session_replay/instrument/index.js +9 -0
- package/src/features/session_replay/shared/recorder.js +2 -1
- package/src/features/soft_navigations/aggregate/ajax-node.js +10 -7
- package/src/features/soft_navigations/aggregate/interaction.js +6 -5
- package/src/features/utils/aggregate-base.js +0 -12
- package/src/features/utils/instrument-base.js +71 -25
- package/src/loaders/agent.js +2 -1
- package/src/loaders/api/consent.js +2 -2
- package/src/loaders/features/features.js +1 -0
package/src/common/unload/eol.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2020-
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import { globalScope, isWorkerScope, isBrowserScope } from '../constants/runtime'
|
|
@@ -22,9 +22,9 @@ if (isWorkerScope) {
|
|
|
22
22
|
* This is used, for example, to submit a final harvest and send all remaining data on best-effort.
|
|
23
23
|
* @param {function} cb - func to run before or during the last reliable event or time of an env's life span
|
|
24
24
|
*/
|
|
25
|
-
export function subscribeToEOL (cb
|
|
25
|
+
export function subscribeToEOL (cb) {
|
|
26
26
|
if (isBrowserScope) {
|
|
27
|
-
subscribeToVisibilityChange(cb, true
|
|
27
|
+
subscribeToVisibilityChange(cb, true) // when user switches tab or hides window, esp. mobile scenario
|
|
28
28
|
} else if (isWorkerScope) {
|
|
29
29
|
globalScope.cleanupTasks.push(cb) // close() should run these tasks before quitting thread
|
|
30
30
|
}
|
|
@@ -68,7 +68,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event'
|
|
|
68
68
|
* | 50 | Failed to connect. Cannot allow registered API. |
|
|
69
69
|
* | 51 | Container agent is not available to register with. Can not connect. |
|
|
70
70
|
* | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
|
|
71
|
-
* | 53 |
|
|
71
|
+
* | 53 | Failed to parse connect response. |
|
|
72
72
|
* | 54 | An experimental feature is being used. Support can not be offered for issues. |
|
|
73
73
|
* | 55 | Register API has been disabled on the container agent. |
|
|
74
74
|
* | 56 | Could not find a matching entity to store data. |
|
|
@@ -94,6 +94,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event'
|
|
|
94
94
|
* | 76 | Agent rejected post message, could not match with existing entity. |
|
|
95
95
|
* | 77 | Agent rejected post message, could not validate origin. |
|
|
96
96
|
* | 78 | RegisteredIframeEntity could not determine parent origin and will not register, to avoid trusting messages from any origin. |
|
|
97
|
+
* | 79 | Unable to initialize Connector and/or Harvester. |
|
|
97
98
|
*
|
|
98
99
|
* @param {number} code The warning code to emit, which will be used to link to the warning code documentation
|
|
99
100
|
* @param {*} [secondary] Secondary data to include, usually an extra message, error or object
|
|
@@ -19,7 +19,7 @@ export function activateFeatures (flags, agentRef) {
|
|
|
19
19
|
// let any window level subscribers know that the agent is running, per install docs
|
|
20
20
|
dispatchGlobalEvent({
|
|
21
21
|
loaded: true,
|
|
22
|
-
drained:
|
|
22
|
+
drained: !agentRef.init.feature_flags.includes('rum_v2'), // v2 RUM always call activateFeatures before any group is drained (in waitForFlags)
|
|
23
23
|
type: 'lifecycle',
|
|
24
24
|
name: 'load',
|
|
25
25
|
feature: undefined,
|
|
@@ -124,6 +124,9 @@ const observePerformance = (observers, config, onEntry) => {
|
|
|
124
124
|
export function trackMFEVitals (target, timings) {
|
|
125
125
|
let fcpObservedAt
|
|
126
126
|
let lcpObservedAt
|
|
127
|
+
// Once disconnect() runs, no further vitals data should be written - even from an observer callback that was
|
|
128
|
+
// already in flight when disconnect() was called.
|
|
129
|
+
let disconnected = false
|
|
127
130
|
|
|
128
131
|
const getTimeRelativeToScriptStart = (capturedAt) => {
|
|
129
132
|
if (capturedAt == null) return capturedAt
|
|
@@ -132,12 +135,21 @@ export function trackMFEVitals (target, timings) {
|
|
|
132
135
|
|
|
133
136
|
const vitals = {
|
|
134
137
|
fcp: {
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
// A clock freeze/tab suspend between script registration and first paint can make this value huge once the
|
|
139
|
+
// tab resumes. Rather than report a multi-hour "FCP", drop it so bad data doesn't reach the backend.
|
|
140
|
+
get value () {
|
|
141
|
+
const relative = getTimeRelativeToScriptStart(fcpObservedAt)
|
|
142
|
+
return relative > CORRELATION_STALE_THRESHOLD_MS ? undefined : relative
|
|
143
|
+
},
|
|
144
|
+
set value (v) { if (!disconnected) fcpObservedAt = v }
|
|
137
145
|
},
|
|
138
146
|
lcp: {
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
// LCP can never occur before FCP, so if FCP was dropped as invalid, LCP is derived from the same untrustworthy clock and must be dropped too.
|
|
148
|
+
get value () {
|
|
149
|
+
if (vitals.fcp.value === undefined) return undefined
|
|
150
|
+
return getTimeRelativeToScriptStart(lcpObservedAt)
|
|
151
|
+
},
|
|
152
|
+
set value (v) { if (!disconnected) lcpObservedAt = v }
|
|
141
153
|
},
|
|
142
154
|
cls: {
|
|
143
155
|
value: undefined
|
|
@@ -159,15 +171,22 @@ export function trackMFEVitals (target, timings) {
|
|
|
159
171
|
}, CORRELATION_STALE_THRESHOLD_MS)
|
|
160
172
|
|
|
161
173
|
const populateVitalMinimums = () => {
|
|
162
|
-
|
|
174
|
+
if (disconnected) return
|
|
175
|
+
if (fcpObservedAt == null) {
|
|
176
|
+
const capturedAt = now()
|
|
177
|
+
// If the very first observed render already exceeds the window, the clock is untrustworthy for this
|
|
178
|
+
// MFE (e.g. tab was frozen/suspended between registration and paint) - shut everything down instead
|
|
179
|
+
// of seeding vitals with data derived from it.
|
|
180
|
+
if (getTimeRelativeToScriptStart(capturedAt) > CORRELATION_STALE_THRESHOLD_MS) {
|
|
181
|
+
vitals.disconnect()
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
fcpObservedAt = capturedAt
|
|
185
|
+
}
|
|
163
186
|
lcpObservedAt ??= now()
|
|
164
187
|
vitals.cls.value ??= 0
|
|
165
188
|
}
|
|
166
189
|
|
|
167
|
-
// if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
|
|
168
|
-
const existingRoots = globalScope.document?.querySelectorAll(`[data-nr-mfe-id="${escapeSelectorValue(target.id)}"]`)
|
|
169
|
-
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums()
|
|
170
|
-
|
|
171
190
|
// Track FCP - first contentful paint
|
|
172
191
|
const fcpObs = observeMutations(target, (_, obs) => {
|
|
173
192
|
// An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
|
|
@@ -183,12 +202,13 @@ export function trackMFEVitals (target, timings) {
|
|
|
183
202
|
|
|
184
203
|
try {
|
|
185
204
|
resizeObs = new globalScope.ResizeObserver((entries) => {
|
|
205
|
+
if (disconnected) return
|
|
186
206
|
entries.forEach((entry) => {
|
|
187
207
|
try {
|
|
188
208
|
const size = entry.contentRect.width * entry.contentRect.height
|
|
189
209
|
if (size > largestSize) {
|
|
190
210
|
largestSize = size
|
|
191
|
-
|
|
211
|
+
vitals.lcp.value = now()
|
|
192
212
|
}
|
|
193
213
|
resizeObs.unobserve(entry.target)
|
|
194
214
|
} catch (e) {
|
|
@@ -245,7 +265,7 @@ export function trackMFEVitals (target, timings) {
|
|
|
245
265
|
// Track CLS - cumulative layout shift
|
|
246
266
|
// Initialize CLS to 0 if browser supports it
|
|
247
267
|
observePerformance(observers, { type: 'layout-shift', buffered: true }, (entry) => {
|
|
248
|
-
if (entry.hadRecentInput) return
|
|
268
|
+
if (disconnected || entry.hadRecentInput) return
|
|
249
269
|
;(entry.sources || []).some(source => {
|
|
250
270
|
if (isInMFE(source.node, target)) {
|
|
251
271
|
// an observed "CLS" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
@@ -259,7 +279,7 @@ export function trackMFEVitals (target, timings) {
|
|
|
259
279
|
|
|
260
280
|
// Track INP - interaction to next paint
|
|
261
281
|
observePerformance(observers, { type: 'event', buffered: true, durationThreshold: 40 }, (entry) => {
|
|
262
|
-
if (!entry.interactionId || !isInMFE(entry.target, target)) return
|
|
282
|
+
if (disconnected || !entry.interactionId || !isInMFE(entry.target, target)) return
|
|
263
283
|
if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
|
|
264
284
|
// an observed "INP" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
265
285
|
populateVitalMinimums()
|
|
@@ -276,6 +296,7 @@ export function trackMFEVitals (target, timings) {
|
|
|
276
296
|
|
|
277
297
|
// Disconnect all observers
|
|
278
298
|
vitals.disconnect = () => {
|
|
299
|
+
disconnected = true
|
|
279
300
|
// Disconnect all observers
|
|
280
301
|
observers.forEach(obs => {
|
|
281
302
|
try {
|
|
@@ -317,5 +338,11 @@ export function trackMFEVitals (target, timings) {
|
|
|
317
338
|
globalScope.addEventListener(type, vitals.disconnect, { once: true, passive: true })
|
|
318
339
|
})
|
|
319
340
|
|
|
341
|
+
// If the MFE has already rendered something on the page before we could set up listeners, just populate vital
|
|
342
|
+
// minimums immediately. Done last, once `observers` is fully populated and `vitals.disconnect` has its real
|
|
343
|
+
// implementation, so a stale-clock detection here can actually tear everything down instead of no-op'ing.
|
|
344
|
+
const existingRoots = globalScope.document?.querySelectorAll(`[data-nr-mfe-id="${escapeSelectorValue(target.id)}"]`)
|
|
345
|
+
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums()
|
|
346
|
+
|
|
320
347
|
return vitals
|
|
321
348
|
}
|
|
@@ -157,7 +157,7 @@ export class Aggregate extends AggregateBase {
|
|
|
157
157
|
serializer (eventBuffer) {
|
|
158
158
|
if (!eventBuffer.length) return
|
|
159
159
|
|
|
160
|
-
const { addString, addStringWithTruncation } = createStringAdders(getAddStringContext, this.obfuscator)
|
|
160
|
+
const { addString, addStringRaw, addStringWithTruncation } = createStringAdders(getAddStringContext, this.obfuscator)
|
|
161
161
|
|
|
162
162
|
let payload = 'bel.7;'
|
|
163
163
|
|
|
@@ -174,16 +174,16 @@ export class Aggregate extends AggregateBase {
|
|
|
174
174
|
numeric(event.endTime - event.startTime),
|
|
175
175
|
numeric(0), // callbackEnd
|
|
176
176
|
numeric(0), // no callbackDuration for non-SPA events
|
|
177
|
-
|
|
177
|
+
addStringRaw(event.method),
|
|
178
178
|
numeric(event.status),
|
|
179
179
|
addString(event.domain),
|
|
180
180
|
addString(event.path),
|
|
181
181
|
numeric(event.requestSize),
|
|
182
182
|
numeric(event.responseSize),
|
|
183
183
|
event.type === 'fetch' ? 1 : '',
|
|
184
|
-
|
|
185
|
-
nullable(event.spanId,
|
|
186
|
-
nullable(event.traceId,
|
|
184
|
+
addStringRaw(0), // nodeId
|
|
185
|
+
nullable(event.spanId, addStringRaw, true) + // guid
|
|
186
|
+
nullable(event.traceId, addStringRaw, true) + // traceId
|
|
187
187
|
nullable(event.spanTimestamp, numeric, false) // timestamp
|
|
188
188
|
]
|
|
189
189
|
|
|
@@ -193,12 +193,15 @@ export class Aggregate extends AggregateBase {
|
|
|
193
193
|
const jsAttributes = this.agentRef.info.jsAttributes
|
|
194
194
|
|
|
195
195
|
// Regular attributes: obfuscate only
|
|
196
|
-
|
|
196
|
+
let regularAttrs = addCustomAttributes({
|
|
197
197
|
...(jsAttributes || {}),
|
|
198
|
-
[AJAX_ID]: event[AJAX_ID], // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
199
198
|
...(event.targetAttributes || {}), // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
|
|
200
199
|
...(event.gql || {})
|
|
201
|
-
}, addString)
|
|
200
|
+
}, { addKey: addStringRaw, addVal: addString })
|
|
201
|
+
|
|
202
|
+
regularAttrs.push(...addCustomAttributes({
|
|
203
|
+
[AJAX_ID]: event[AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
204
|
+
}, { addKey: addStringRaw, addVal: addStringRaw }))
|
|
202
205
|
|
|
203
206
|
// Payload attributes: obfuscate then truncate
|
|
204
207
|
const payloadAttrs = addCustomAttributes({
|
|
@@ -207,7 +210,7 @@ export class Aggregate extends AggregateBase {
|
|
|
207
210
|
...(event.requestQuery ? { requestQuery: event.requestQuery } : {}),
|
|
208
211
|
...(event.responseBody ? { responseBody: event.responseBody } : {}),
|
|
209
212
|
...(event.responseHeaders ? { responseHeaders: event.responseHeaders } : {})
|
|
210
|
-
}, addStringWithTruncation)
|
|
213
|
+
}, { addKey: addStringRaw, addVal: addStringWithTruncation })
|
|
211
214
|
|
|
212
215
|
const attrParts = [...regularAttrs, ...payloadAttrs]
|
|
213
216
|
fields.unshift(numeric(attrParts.length))
|
|
@@ -132,9 +132,7 @@ export class Aggregate extends AggregateBase {
|
|
|
132
132
|
}
|
|
133
133
|
this.events.add(body)
|
|
134
134
|
|
|
135
|
-
if (this.agentRef.runtime.harvester.triggerHarvestFor(this
|
|
136
|
-
sendEmptyBody: true
|
|
137
|
-
}).ranSend) this.sentRum = true
|
|
135
|
+
if (this.agentRef.runtime.harvester.triggerHarvestFor(this).ranSend) this.sentRum = true
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
serializer (eventBuffer) { // this is necessary because PVE sends a single item rather than an array; in the case of undefined, this prevents sending [null] as body
|
|
@@ -162,9 +160,7 @@ export class Aggregate extends AggregateBase {
|
|
|
162
160
|
super.postHarvestCleanup({ sent, retry: retry && !hasCachedRumResponse }) // this will set isRetrying & re-buffer the body if request is to be retried
|
|
163
161
|
|
|
164
162
|
if (this.isRetrying && this.retries++ < 1) { // Only retry once
|
|
165
|
-
setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this,
|
|
166
|
-
sendEmptyBody: true
|
|
167
|
-
}), 5000) // Retry sending the RUM event after 5 seconds
|
|
163
|
+
setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this), 5000) // Retry sending the RUM event after 5 seconds
|
|
168
164
|
return
|
|
169
165
|
}
|
|
170
166
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { globalScope, isBrowserScope, originTime, getNavigationEntry } from '../../../common/constants/runtime'
|
|
6
|
+
import { addPT, addPN } from '../../../common/timing/nav-timing'
|
|
7
|
+
import { stringify } from '../../../common/util/stringify'
|
|
8
|
+
import * as CONSTANTS from '../constants'
|
|
9
|
+
import { getActivatedFeaturesFlags } from '../aggregate/initialized-features'
|
|
10
|
+
import { AggregateBase } from '../../utils/aggregate-base'
|
|
11
|
+
import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
|
|
12
|
+
import { firstPaint } from '../../../common/vitals/first-paint'
|
|
13
|
+
import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
|
|
14
|
+
import { Obfuscator } from '../../../common/util/obfuscate'
|
|
15
|
+
import { webdriverDetected } from '../../../common/util/webdriver-detection'
|
|
16
|
+
import { EVENT_TYPES } from '../../../common/constants/events'
|
|
17
|
+
|
|
18
|
+
export class Aggregate extends AggregateBase {
|
|
19
|
+
static featureName = CONSTANTS.FEATURE_NAME
|
|
20
|
+
|
|
21
|
+
#timeToFirstByte = 0
|
|
22
|
+
#firstByteToWindowLoad = 0 // our "frontend" duration
|
|
23
|
+
#firstByteToDomContent = 0 // our "dom processing" duration
|
|
24
|
+
#obfuscator
|
|
25
|
+
|
|
26
|
+
constructor (agentRef) {
|
|
27
|
+
super(agentRef, CONSTANTS.FEATURE_NAME)
|
|
28
|
+
this.#obfuscator = new Obfuscator(agentRef, EVENT_TYPES.PVE)
|
|
29
|
+
|
|
30
|
+
if (isBrowserScope) {
|
|
31
|
+
timeToFirstByte.subscribe(({ value, attrs }) => {
|
|
32
|
+
const navEntry = attrs.navigationEntry
|
|
33
|
+
this.#timeToFirstByte = Math.max(value, this.#timeToFirstByte)
|
|
34
|
+
this.#firstByteToWindowLoad = Math.max(Math.round(navEntry.loadEventEnd - this.#timeToFirstByte), this.#firstByteToWindowLoad) // our "frontend" duration
|
|
35
|
+
this.#firstByteToDomContent = Math.max(Math.round(navEntry.domContentLoadedEventEnd - this.#timeToFirstByte), this.#firstByteToDomContent) // our "dom processing" duration
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.waitForFlags(([])).then(() => {
|
|
40
|
+
const body = this.#obfuscator.traverseAndObfuscateEvents({ ja: { ...agentRef.info.jsAttributes, webdriverDetected } })
|
|
41
|
+
this.events.add(body)
|
|
42
|
+
this.drain()
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
queryStringsBuilder () {
|
|
47
|
+
const info = this.agentRef.info
|
|
48
|
+
const measures = {}
|
|
49
|
+
|
|
50
|
+
if (info.queueTime) measures.qt = info.queueTime
|
|
51
|
+
if (info.applicationTime) measures.ap = info.applicationTime
|
|
52
|
+
|
|
53
|
+
// These 3 values should've been recorded after load and before this func runs. They are part of the minimum required for PageView events to be created.
|
|
54
|
+
// Following PR #428, which demands that the BA must drive entity indexing, these need to be sent even outside of the main window context where PerformanceTiming
|
|
55
|
+
// or PerformanceNavigationTiming do not exists. Hence, they'll be filled in by 0s instead in, for example, worker threads that still init the PVE module.
|
|
56
|
+
measures.be = this.#timeToFirstByte
|
|
57
|
+
measures.fe = this.#firstByteToWindowLoad
|
|
58
|
+
measures.dc = this.#firstByteToDomContent
|
|
59
|
+
|
|
60
|
+
const queryParameters = {
|
|
61
|
+
at: info.atts,
|
|
62
|
+
af: getActivatedFeaturesFlags(this.agentRef).join(','),
|
|
63
|
+
igp: this.agentRef.runtime.appMetadata.igp,
|
|
64
|
+
...measures,
|
|
65
|
+
fp: firstPaint.current.value,
|
|
66
|
+
fcp: firstContentfulPaint.current.value
|
|
67
|
+
}
|
|
68
|
+
if (globalScope.performance) {
|
|
69
|
+
const navTimingEntry = getNavigationEntry()
|
|
70
|
+
if (navTimingEntry) { // Navigation Timing level 2 API that replaced PerformanceTiming & PerformanceNavigation
|
|
71
|
+
const perf = ({
|
|
72
|
+
timing: addPT(originTime, navTimingEntry, {}),
|
|
73
|
+
navigation: addPN(navTimingEntry, {})
|
|
74
|
+
})
|
|
75
|
+
queryParameters.perf = stringify(perf)
|
|
76
|
+
} else if (typeof PerformanceTiming !== 'undefined') { // Modern Safari iFrames and Safari pre-15 do not support level 2 timing.
|
|
77
|
+
const perf = ({
|
|
78
|
+
timing: addPT(originTime, globalScope.performance.timing, {}, true),
|
|
79
|
+
navigation: addPN(globalScope.performance.navigation, {})
|
|
80
|
+
})
|
|
81
|
+
queryParameters.perf = stringify(perf)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const { correctedOriginTime } = this.agentRef.runtime.timeKeeper
|
|
85
|
+
if (correctedOriginTime) queryParameters.timestamp = correctedOriginTime // a "PageView" is set to represent the page's (server adjusted) origin time rather than when event is recorded/harvested
|
|
86
|
+
|
|
87
|
+
return queryParameters
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get harvestEndpointVersion () {
|
|
91
|
+
return 2
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
serializer (eventBuffer) { // this is necessary because PVE sends a single item rather than an array; in the case of undefined, this prevents sending [null] as body
|
|
95
|
+
return eventBuffer[0]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -20,7 +20,12 @@ export class Instrument extends InstrumentBase {
|
|
|
20
20
|
/** feature specific APIs */
|
|
21
21
|
setupSetPageViewNameAPI(agentRef)
|
|
22
22
|
|
|
23
|
-
this.importAggregator(agentRef, () =>
|
|
23
|
+
this.importAggregator(agentRef, () => {
|
|
24
|
+
if (agentRef.init.feature_flags.includes('rum_v2')) {
|
|
25
|
+
return import(/* webpackChunkName: "page_view_event-aggregate" */ '../aggregate_v2')
|
|
26
|
+
}
|
|
27
|
+
return import(/* webpackChunkName: "page_view_event-aggregate" */ '../aggregate')
|
|
28
|
+
})
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
setupInspectionEvents () {
|
|
@@ -23,6 +23,7 @@ import { loadTime } from '../../../common/vitals/load-time'
|
|
|
23
23
|
import { webdriverDetected } from '../../../common/util/webdriver-detection'
|
|
24
24
|
import { cleanURL } from '../../../common/url/clean-url'
|
|
25
25
|
import { EVENT_TYPES } from '../../../common/constants/events'
|
|
26
|
+
import { createStringAdders } from '../../../common/payloads/payloads'
|
|
26
27
|
|
|
27
28
|
export class Aggregate extends AggregateBase {
|
|
28
29
|
static featureName = FEATURE_NAME
|
|
@@ -58,7 +59,11 @@ export class Aggregate extends AggregateBase {
|
|
|
58
59
|
const { name, value, attrs } = cumulativeLayoutShift.current
|
|
59
60
|
if (value === undefined) return
|
|
60
61
|
this.addTiming(name, value * 1000, attrs)
|
|
61
|
-
}, true, true) // CLS node should only
|
|
62
|
+
}, true, true) // CLS node should only report on vis change rather than on every change.
|
|
63
|
+
/* NOTE: the capture=true is required -- empirically verified against a real browser (Chrome) for CLS
|
|
64
|
+
timing node to reliably be in the same final harvest as the rest, alongside Harvester#startTimer's
|
|
65
|
+
queueMicrotask deferral of the EOL harvest itself. Dropping this flag caused CLS to be split into its own,
|
|
66
|
+
separate, later harvest instead of going out together with pageHide/INP. */
|
|
62
67
|
|
|
63
68
|
this.drain()
|
|
64
69
|
})
|
|
@@ -93,6 +98,8 @@ export class Aggregate extends AggregateBase {
|
|
|
93
98
|
attrs.cls = cumulativeLayoutShift.current.value
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
attrs.webdriverDetected = webdriverDetected
|
|
102
|
+
|
|
96
103
|
const timing = {
|
|
97
104
|
name,
|
|
98
105
|
value,
|
|
@@ -125,17 +132,13 @@ export class Aggregate extends AggregateBase {
|
|
|
125
132
|
})
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
#getGlobalCustomAttributes () {
|
|
136
|
+
const reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type',
|
|
137
|
+
'net-etype', 'net-rtt', 'net-dlink', 'webdriverDetected']
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (reservedAttributes.indexOf(key) < 0) {
|
|
135
|
-
timingAttributes[key] = val
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
timingAttributes.webdriverDetected = webdriverDetected
|
|
139
|
+
return Object.fromEntries(
|
|
140
|
+
Object.entries(this.agentRef.info.jsAttributes || {}).filter(([key]) => !reservedAttributes.includes(key))
|
|
141
|
+
)
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
preHarvestChecks () {
|
|
@@ -146,7 +149,9 @@ export class Aggregate extends AggregateBase {
|
|
|
146
149
|
// serialize array of timing data
|
|
147
150
|
serializer (eventBuffer) {
|
|
148
151
|
if (!eventBuffer?.length) return ''
|
|
149
|
-
|
|
152
|
+
const { addString, addStringRaw } = createStringAdders(getAddStringContext, this.obfuscator)
|
|
153
|
+
const keepOrigValue = { addKey: addStringRaw, addVal: addStringRaw }
|
|
154
|
+
const obfuscateValue = { addKey: addStringRaw, addVal: addString }
|
|
150
155
|
|
|
151
156
|
var payload = 'bel.6;'
|
|
152
157
|
|
|
@@ -154,12 +159,16 @@ export class Aggregate extends AggregateBase {
|
|
|
154
159
|
var timing = eventBuffer[i]
|
|
155
160
|
|
|
156
161
|
payload += 'e,'
|
|
157
|
-
payload +=
|
|
162
|
+
payload += addStringRaw(timing.name) + ','
|
|
158
163
|
payload += nullable(timing.value, numeric, false) + ','
|
|
159
164
|
|
|
160
|
-
this
|
|
165
|
+
const userAttrs = this.#getGlobalCustomAttributes()
|
|
166
|
+
|
|
167
|
+
const { pageUrl, ...systemReservedAttrs } = timing.attrs || {}
|
|
168
|
+
var attrParts = addCustomAttributes(systemReservedAttrs, keepOrigValue)
|
|
169
|
+
attrParts.push(...addCustomAttributes(userAttrs, obfuscateValue))
|
|
170
|
+
if (pageUrl) attrParts.push(...addCustomAttributes({ pageUrl }, obfuscateValue))
|
|
161
171
|
|
|
162
|
-
var attrParts = addCustomAttributes(timing.attrs, addString)
|
|
163
172
|
if (attrParts && attrParts.length > 0) {
|
|
164
173
|
payload += numeric(attrParts.length) + ';' + attrParts.join(';')
|
|
165
174
|
}
|
|
@@ -46,6 +46,10 @@ export class Instrument extends InstrumentBase {
|
|
|
46
46
|
}) // could handle specific fail-state behaviors with a .catch block here
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// If the aggregate never successfully loads (missing session prerequisite, or the aggregate module itself
|
|
50
|
+
// fails to import/construct), any preload recording that already started needs to be stopped.
|
|
51
|
+
this.abortHandler = this.#abort
|
|
52
|
+
|
|
49
53
|
this.importAggregator(this.agentRef, () => import(/* webpackChunkName: "session_replay-aggregate" */ '../aggregate'), this)
|
|
50
54
|
|
|
51
55
|
/** If the recorder is running, we can pass error events on to the agg to help it switch to full mode later */
|
|
@@ -109,6 +113,11 @@ export class Instrument extends InstrumentBase {
|
|
|
109
113
|
}) // could handle specific fail-state behaviors with a .catch block here
|
|
110
114
|
}
|
|
111
115
|
}
|
|
116
|
+
|
|
117
|
+
/** Stops any preload recording that already started if the aggregate never ends up successfully loading. */
|
|
118
|
+
#abort () {
|
|
119
|
+
this.recorder?.stopRecording()
|
|
120
|
+
}
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
export const SessionReplay = Instrument
|
|
@@ -204,7 +204,8 @@ export class Recorder {
|
|
|
204
204
|
|
|
205
205
|
// We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
|
|
206
206
|
// it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
|
|
207
|
-
|
|
207
|
+
// (the aggregate may not exist yet/at all if its own bootstrap hasn't finished or failed -- nothing to harvest for in that case.)
|
|
208
|
+
if (((this.events.hasSnapshot && this.events.hasMeta) || payloadSize > IDEAL_PAYLOAD_SIZE) && !this.isErrorMode && this.srInstrument.featAggregate) {
|
|
208
209
|
// if we've made it to the ideal size of ~16kb before the interval timer, we should send early.
|
|
209
210
|
this.agentRef.runtime.harvester.triggerHarvestFor(this.srInstrument.featAggregate)
|
|
210
211
|
}
|
|
@@ -42,7 +42,7 @@ export class AjaxNode extends BelNode {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
serialize (parentStartTimestamp, agentRef, ajaxObfuscator) {
|
|
45
|
-
const { addString, addStringWithTruncation } = createStringAdders(getAddStringContext, ajaxObfuscator)
|
|
45
|
+
const { addString, addStringRaw, addStringWithTruncation } = createStringAdders(getAddStringContext, ajaxObfuscator)
|
|
46
46
|
|
|
47
47
|
const nodeList = []
|
|
48
48
|
|
|
@@ -54,22 +54,25 @@ export class AjaxNode extends BelNode {
|
|
|
54
54
|
numeric(this.end - this.start), // end is relative to start
|
|
55
55
|
numeric(this.callbackEnd - this.end), // callbackEnd is relative to end
|
|
56
56
|
numeric(this.callbackDuration), // not relative
|
|
57
|
-
|
|
57
|
+
addStringRaw(this.method),
|
|
58
58
|
numeric(this.status),
|
|
59
59
|
addString(this.domain),
|
|
60
60
|
addString(this.path),
|
|
61
61
|
numeric(this.txSize),
|
|
62
62
|
numeric(this.rxSize),
|
|
63
63
|
this.requestedWith,
|
|
64
|
-
|
|
65
|
-
nullable(this.spanId,
|
|
64
|
+
addStringRaw(this.nodeId),
|
|
65
|
+
nullable(this.spanId, addStringRaw, true) + nullable(this.traceId, addStringRaw, true) + nullable(this.spanTimestamp, numeric)
|
|
66
66
|
]
|
|
67
67
|
// Regular attributes: obfuscate only
|
|
68
68
|
const regularAttrs = addCustomAttributes({
|
|
69
|
-
[AJAX_ID]: this[AJAX_ID],
|
|
70
69
|
...(this.targetAttributes || {}),
|
|
71
70
|
...(this.gql || {})
|
|
72
|
-
}, addString)
|
|
71
|
+
}, { addKey: addStringRaw, addVal: addString })
|
|
72
|
+
|
|
73
|
+
regularAttrs.push(...addCustomAttributes({
|
|
74
|
+
[AJAX_ID]: this[AJAX_ID]
|
|
75
|
+
}, { addKey: addStringRaw, addVal: addStringRaw }))
|
|
73
76
|
|
|
74
77
|
// Payload attributes: obfuscate then truncate
|
|
75
78
|
const payloadAttrs = addCustomAttributes({
|
|
@@ -78,7 +81,7 @@ export class AjaxNode extends BelNode {
|
|
|
78
81
|
...(this.requestQuery ? { requestQuery: this.requestQuery } : {}),
|
|
79
82
|
...(this.responseBody ? { responseBody: this.responseBody } : {}),
|
|
80
83
|
...(this.responseHeaders ? { responseHeaders: this.responseHeaders } : {})
|
|
81
|
-
}, addStringWithTruncation)
|
|
84
|
+
}, { addKey: addStringRaw, addVal: addStringWithTruncation })
|
|
82
85
|
|
|
83
86
|
let allAttachedNodes = [...regularAttrs, ...payloadAttrs]
|
|
84
87
|
this.children.forEach(node => allAttachedNodes.push(node.serialize())) // no children is expected under ajax nodes at this time
|
|
@@ -9,6 +9,7 @@ import { now } from '../../../common/timing/now'
|
|
|
9
9
|
import { cleanURL } from '../../../common/url/clean-url'
|
|
10
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
|
+
import { createStringAdders } from '../../../common/payloads/payloads'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* link https://github.com/newrelic/nr-querypack/blob/main/schemas/bel/7.qpschema
|
|
@@ -139,7 +140,7 @@ export class Interaction extends BelNode {
|
|
|
139
140
|
*/
|
|
140
141
|
serialize (firstStartTimeOfPayload, agentRef, interactionObfuscator, ajaxObfuscator) {
|
|
141
142
|
const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined
|
|
142
|
-
const addString = getAddStringContext
|
|
143
|
+
const { addString, addStringRaw } = createStringAdders(getAddStringContext, interactionObfuscator)
|
|
143
144
|
const nodeList = []
|
|
144
145
|
let ixnType
|
|
145
146
|
if (this.trigger === IPL_TRIGGER_NAME) ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD
|
|
@@ -154,7 +155,7 @@ export class Interaction extends BelNode {
|
|
|
154
155
|
numeric(this.end - this.start), // end -- relative to start
|
|
155
156
|
numeric(0), // callbackEnd -- relative to start; not used by BrowserInteraction events so these are always 0
|
|
156
157
|
numeric(0), // not relative; always 0 for BrowserInteraction
|
|
157
|
-
|
|
158
|
+
addStringRaw(this.trigger),
|
|
158
159
|
addString(cleanURL(this.initialPageURL, true)),
|
|
159
160
|
addString(cleanURL(this.oldURL, true)),
|
|
160
161
|
addString(cleanURL(this.newURL, true)),
|
|
@@ -162,12 +163,12 @@ export class Interaction extends BelNode {
|
|
|
162
163
|
ixnType,
|
|
163
164
|
nullable(this.queueTime, numeric, true) + nullable(this.appTime, numeric, true) +
|
|
164
165
|
nullable(this.oldRoute, addString, true) + nullable(this.newRoute, addString, true) +
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
addStringRaw(this.id),
|
|
167
|
+
addStringRaw(this.nodeId),
|
|
167
168
|
nullable(this.firstPaint, numeric, true) + nullable(this.firstContentfulPaint, numeric)
|
|
168
169
|
]
|
|
169
170
|
const customAttributes = { ...agentRef.info.jsAttributes, ...this.customAttributes } // attrs specific to this interaction should have precedence over the general custom attrs
|
|
170
|
-
const allAttachedNodes = addCustomAttributes(customAttributes || {}, addString) // start with all custom attributes
|
|
171
|
+
const allAttachedNodes = addCustomAttributes(customAttributes || {}, { addKey: addStringRaw, addVal: addString }) // start with all custom attributes
|
|
171
172
|
if (agentRef.info.atts) allAttachedNodes.push('a,' + addString(agentRef.info.atts)) // add apm provided attributes
|
|
172
173
|
/* Querypack encoder+decoder quirkiness:
|
|
173
174
|
- If first ixn node of payload is being processed, its children's start time must be offset by this node's start. (firstStartTime should be undefined.)
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { FeatureBase } from './feature-base'
|
|
6
6
|
import { drain } from '../../common/drain/drain'
|
|
7
7
|
import { FEATURE_NAMES } from '../../loaders/features/features'
|
|
8
|
-
import { Harvester } from '../../common/harvest/harvester'
|
|
9
8
|
import { EventBuffer } from './event-buffer'
|
|
10
9
|
import { handle } from '../../common/event-emitter/handle'
|
|
11
10
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../metrics/constants'
|
|
@@ -21,7 +20,6 @@ export class AggregateBase extends FeatureBase {
|
|
|
21
20
|
*/
|
|
22
21
|
constructor (agentRef, featureName) {
|
|
23
22
|
super(agentRef, featureName)
|
|
24
|
-
this.doOnceForAllAggregate(agentRef)
|
|
25
23
|
|
|
26
24
|
/** @type {Boolean} indicates if custom attributes are combined in each event payload for size estimation purposes. this is set to true in derived classes that need to evaluate custom attributes separately from the event payload */
|
|
27
25
|
this.customAttributesAreSeparate = false
|
|
@@ -178,16 +176,6 @@ export class AggregateBase extends FeatureBase {
|
|
|
178
176
|
this.events.clearSave(this.harvestOpts)
|
|
179
177
|
}
|
|
180
178
|
|
|
181
|
-
/**
|
|
182
|
-
* These are actions related to shared resources that should be initialized once by whichever feature Aggregate subclass loads first.
|
|
183
|
-
* This method should run after checkConfiguration, which may reset the agent's info/runtime object that is used here.
|
|
184
|
-
*/
|
|
185
|
-
doOnceForAllAggregate (agentRef) {
|
|
186
|
-
// Note: obfuscator is now created per-feature for their specific event types
|
|
187
|
-
|
|
188
|
-
if (!agentRef.runtime.harvester) agentRef.runtime.harvester = new Harvester(agentRef)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
179
|
/**
|
|
192
180
|
* Report a supportability metric
|
|
193
181
|
* @param {*} metricName The tag of the name matching the Angler aggregation tag
|