@newrelic/browser-agent 1.316.0 → 1.317.0-rc.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/config/configurable.js +1 -1
- package/dist/cjs/common/config/init-types.js +1 -0
- package/dist/cjs/common/config/init.js +6 -4
- package/dist/cjs/common/constants/env.cdn.js +2 -2
- package/dist/cjs/common/constants/env.npm.js +2 -2
- package/dist/cjs/common/constants/events.js +26 -0
- package/dist/cjs/common/harvest/harvester.js +9 -217
- package/dist/cjs/common/harvest/send.js +232 -0
- package/dist/cjs/common/harvest/types.js +9 -5
- package/dist/cjs/common/payloads/payloads.js +127 -0
- package/dist/cjs/common/serialize/bel-serializer.js +4 -3
- package/dist/cjs/common/session/session-entity.js +8 -8
- package/dist/cjs/common/util/data-size.js +2 -2
- package/dist/cjs/common/util/obfuscate.js +147 -3
- package/dist/cjs/common/v2/mfe-vitals.js +286 -0
- package/dist/cjs/common/wrap/wrap-websocket.js +2 -1
- package/dist/cjs/common/wrap/wrap-xhr.js +1 -1
- package/dist/cjs/features/ajax/aggregate/gql.js +40 -1
- package/dist/cjs/features/ajax/aggregate/index.js +51 -13
- package/dist/cjs/features/ajax/constants.js +6 -1
- package/dist/cjs/features/ajax/instrument/index.js +105 -17
- package/dist/cjs/features/generic_events/aggregate/index.js +15 -10
- package/dist/cjs/features/generic_events/constants.js +3 -2
- package/dist/cjs/features/jserrors/aggregate/format-stack-trace.js +2 -2
- package/dist/cjs/features/jserrors/aggregate/index.js +12 -3
- package/dist/cjs/features/logging/aggregate/index.js +12 -8
- package/dist/cjs/features/metrics/aggregate/index.js +5 -1
- package/dist/cjs/features/page_view_event/aggregate/index.js +72 -33
- package/dist/cjs/features/page_view_timing/aggregate/index.js +6 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +39 -29
- package/dist/cjs/features/session_trace/aggregate/index.js +35 -29
- package/dist/cjs/features/session_trace/constants.js +17 -3
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +36 -5
- package/dist/cjs/features/soft_navigations/aggregate/index.js +7 -1
- package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +6 -4
- package/dist/cjs/features/utils/aggregate-base.js +4 -3
- package/dist/cjs/loaders/api/register.js +26 -4
- package/dist/esm/common/config/configurable.js +1 -1
- package/dist/esm/common/config/init-types.js +1 -0
- package/dist/esm/common/config/init.js +3 -1
- package/dist/esm/common/constants/env.cdn.js +2 -2
- package/dist/esm/common/constants/env.npm.js +2 -2
- package/dist/esm/common/constants/events.js +20 -0
- package/dist/esm/common/harvest/harvester.js +9 -217
- package/dist/esm/common/harvest/send.js +226 -0
- package/dist/esm/common/harvest/types.js +9 -5
- package/dist/esm/common/payloads/payloads.js +118 -0
- package/dist/esm/common/serialize/bel-serializer.js +4 -3
- package/dist/esm/common/session/session-entity.js +9 -9
- package/dist/esm/common/util/data-size.js +2 -2
- package/dist/esm/common/util/obfuscate.js +147 -3
- package/dist/esm/common/v2/mfe-vitals.js +281 -0
- package/dist/esm/common/wrap/wrap-websocket.js +2 -1
- package/dist/esm/common/wrap/wrap-xhr.js +1 -1
- package/dist/esm/features/ajax/aggregate/gql.js +39 -1
- package/dist/esm/features/ajax/aggregate/index.js +52 -14
- package/dist/esm/features/ajax/constants.js +5 -0
- package/dist/esm/features/ajax/instrument/index.js +106 -18
- package/dist/esm/features/generic_events/aggregate/index.js +15 -10
- package/dist/esm/features/generic_events/constants.js +3 -2
- package/dist/esm/features/jserrors/aggregate/format-stack-trace.js +2 -2
- package/dist/esm/features/jserrors/aggregate/index.js +12 -3
- package/dist/esm/features/logging/aggregate/index.js +12 -8
- package/dist/esm/features/metrics/aggregate/index.js +5 -1
- package/dist/esm/features/page_view_event/aggregate/index.js +71 -32
- package/dist/esm/features/page_view_timing/aggregate/index.js +6 -1
- package/dist/esm/features/session_replay/aggregate/index.js +39 -29
- package/dist/esm/features/session_trace/aggregate/index.js +36 -30
- package/dist/esm/features/session_trace/constants.js +16 -2
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +36 -5
- package/dist/esm/features/soft_navigations/aggregate/index.js +7 -1
- package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +6 -4
- package/dist/esm/features/utils/aggregate-base.js +4 -3
- package/dist/esm/loaders/api/register.js +26 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/config/init-types.d.ts +4 -0
- package/dist/types/common/config/init-types.d.ts.map +1 -1
- package/dist/types/common/config/init.d.ts.map +1 -1
- package/dist/types/common/constants/events.d.ts +16 -0
- package/dist/types/common/constants/events.d.ts.map +1 -0
- package/dist/types/common/harvest/harvester.d.ts +2 -16
- package/dist/types/common/harvest/harvester.d.ts.map +1 -1
- package/dist/types/common/harvest/send.d.ts +9 -0
- package/dist/types/common/harvest/send.d.ts.map +1 -0
- package/dist/types/common/harvest/types.d.ts +39 -10
- package/dist/types/common/harvest/types.d.ts.map +1 -1
- package/dist/types/common/payloads/payloads.d.ts +42 -0
- package/dist/types/common/payloads/payloads.d.ts.map +1 -0
- package/dist/types/common/serialize/bel-serializer.d.ts +1 -1
- package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/util/obfuscate.d.ts +23 -2
- package/dist/types/common/util/obfuscate.d.ts.map +1 -1
- package/dist/types/common/v2/mfe-vitals.d.ts +14 -0
- package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -0
- package/dist/types/common/wrap/wrap-websocket.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/gql.d.ts +8 -0
- package/dist/types/features/ajax/aggregate/gql.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts +2 -0
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/ajax/constants.d.ts +5 -0
- package/dist/types/features/ajax/constants.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/index.d.ts +3 -1
- package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/generic_events/constants.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +8 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/logging/aggregate/index.d.ts +3 -1
- package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/index.d.ts +2 -0
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/index.d.ts +3 -0
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts +2 -0
- package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +4 -2
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +3 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/constants.d.ts +17 -0
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +6 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/index.d.ts +3 -0
- package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +3 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts +0 -1
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/loaders/api/register.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/common/config/configurable.js +1 -1
- package/src/common/config/init-types.js +1 -0
- package/src/common/config/init.js +2 -1
- package/src/common/constants/events.js +20 -0
- package/src/common/harvest/harvester.js +9 -201
- package/src/common/harvest/send.js +209 -0
- package/src/common/harvest/types.js +9 -5
- package/src/common/payloads/payloads.js +135 -0
- package/src/common/serialize/bel-serializer.js +4 -3
- package/src/common/session/session-entity.js +8 -9
- package/src/common/util/data-size.js +2 -2
- package/src/common/util/obfuscate.js +154 -3
- package/src/common/v2/mfe-vitals.js +260 -0
- package/src/common/wrap/wrap-websocket.js +2 -1
- package/src/common/wrap/wrap-xhr.js +1 -1
- package/src/features/ajax/aggregate/gql.js +42 -1
- package/src/features/ajax/aggregate/index.js +43 -13
- package/src/features/ajax/constants.js +5 -1
- package/src/features/ajax/instrument/index.js +114 -18
- package/src/features/generic_events/aggregate/index.js +14 -9
- package/src/features/generic_events/constants.js +3 -2
- package/src/features/jserrors/aggregate/format-stack-trace.js +3 -3
- package/src/features/jserrors/aggregate/index.js +12 -3
- package/src/features/logging/aggregate/index.js +12 -11
- package/src/features/metrics/aggregate/index.js +5 -1
- package/src/features/page_view_event/aggregate/index.js +58 -29
- package/src/features/page_view_timing/aggregate/index.js +6 -1
- package/src/features/session_replay/aggregate/index.js +37 -28
- package/src/features/session_trace/aggregate/index.js +37 -30
- package/src/features/session_trace/constants.js +7 -1
- package/src/features/soft_navigations/aggregate/ajax-node.js +26 -5
- package/src/features/soft_navigations/aggregate/index.js +7 -1
- package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
- package/src/features/soft_navigations/aggregate/interaction.js +6 -4
- package/src/features/utils/aggregate-base.js +3 -3
- package/src/loaders/api/register.js +24 -4
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { globalScope, isBrowserScope } from '../constants/runtime'
|
|
7
|
+
import { now } from '../timing/now'
|
|
8
|
+
import { cleanURL } from '../url/clean-url'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a simple CSS selector for an element
|
|
16
|
+
* @param {Element} elem - The element to generate selector for
|
|
17
|
+
* @returns {string|null} CSS selector or null if element is invalid
|
|
18
|
+
*/
|
|
19
|
+
const getElementSelector = (elem) => {
|
|
20
|
+
try {
|
|
21
|
+
if (!elem || !elem.tagName) return null
|
|
22
|
+
|
|
23
|
+
let selector = elem.tagName.toLowerCase()
|
|
24
|
+
if (elem.id) selector += `#${elem.id}`
|
|
25
|
+
else if (elem.className && typeof elem.className === 'string') {
|
|
26
|
+
const classes = elem.className.trim().split(/\s+/).slice(0, 2).join('.')
|
|
27
|
+
if (classes) selector += `.${classes}`
|
|
28
|
+
}
|
|
29
|
+
return selector
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract URL from an element (for images, videos, etc.)
|
|
37
|
+
* @param {Element} elem - The element to extract URL from
|
|
38
|
+
* @returns {string|null} Cleaned URL or null
|
|
39
|
+
*/
|
|
40
|
+
const getElementURL = (elem) => {
|
|
41
|
+
try {
|
|
42
|
+
if (!elem) return null
|
|
43
|
+
|
|
44
|
+
// For images and videos
|
|
45
|
+
if (elem.src) return cleanURL(elem.src)
|
|
46
|
+
|
|
47
|
+
// For elements with background images
|
|
48
|
+
const bgImage = globalScope.getComputedStyle?.(elem)?.backgroundImage
|
|
49
|
+
if (bgImage && bgImage !== 'none') {
|
|
50
|
+
const match = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/)
|
|
51
|
+
if (match?.[1]) return cleanURL(match[1])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isObservable = (node) => {
|
|
61
|
+
try {
|
|
62
|
+
return node?.textContent?.trim() || ['img', 'video', 'canvas', 'svg'].includes(node?.nodeName?.toLowerCase())
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if node is within a specific MFE
|
|
70
|
+
* @param {Node} node - DOM node to check
|
|
71
|
+
* @param {string} id - MFE ID to match
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
const isInMFE = (node, id) => {
|
|
75
|
+
try {
|
|
76
|
+
let curr = node.nodeType === 1 ? node : node.parentElement
|
|
77
|
+
while (curr?.tagName) {
|
|
78
|
+
if (curr.dataset?.nrMfeId === id) return true
|
|
79
|
+
curr = curr.parentNode
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {}
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create mutation observer for MFE nodes
|
|
87
|
+
* @param {string} id - MFE ID to track
|
|
88
|
+
* @param {Function} onMatch - Callback when matching node is added
|
|
89
|
+
* @returns {MutationObserver}
|
|
90
|
+
*/
|
|
91
|
+
const observeMutations = (id, onMatch) => {
|
|
92
|
+
const obs = new globalScope.MutationObserver(mutations => {
|
|
93
|
+
mutations.forEach(m => m.addedNodes.forEach(node => {
|
|
94
|
+
if (isObservable(node) && isInMFE(node, id)) {
|
|
95
|
+
onMatch(node, obs)
|
|
96
|
+
}
|
|
97
|
+
}))
|
|
98
|
+
})
|
|
99
|
+
obs.observe(globalScope.document, { childList: true, subtree: true })
|
|
100
|
+
return obs
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create performance observer for MFE entries
|
|
105
|
+
* @param {Array} observers - Array to track observers
|
|
106
|
+
* @param {Object} config - Observer configuration
|
|
107
|
+
* @param {Function} onEntry - Callback for each entry
|
|
108
|
+
* @returns {PerformanceObserver|null} Observer if successful, null if unsupported
|
|
109
|
+
*/
|
|
110
|
+
const observePerformance = (observers, config, onEntry) => {
|
|
111
|
+
try {
|
|
112
|
+
const obs = new globalScope.PerformanceObserver(list => {
|
|
113
|
+
list.getEntries().forEach(onEntry)
|
|
114
|
+
})
|
|
115
|
+
obs.observe(config)
|
|
116
|
+
observers.push(obs)
|
|
117
|
+
return obs
|
|
118
|
+
} catch (e) {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Tracks all Core Web Vitals for a specific MFE.
|
|
125
|
+
* @param {string} id - The MFE ID to track
|
|
126
|
+
* @returns {{fcp: object|null, lcp: object|null, cls: object|null, inp: object|null, disconnect: Function}}
|
|
127
|
+
*/
|
|
128
|
+
export function trackMFEVitals (id) {
|
|
129
|
+
const vitals = {
|
|
130
|
+
fcp: null,
|
|
131
|
+
lcp: null,
|
|
132
|
+
cls: null,
|
|
133
|
+
inp: null,
|
|
134
|
+
disconnect: () => {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!id || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals
|
|
138
|
+
|
|
139
|
+
const observers = []
|
|
140
|
+
|
|
141
|
+
// Track FCP - first contentful paint
|
|
142
|
+
observeMutations(id, (node, obs) => {
|
|
143
|
+
if (vitals.fcp === null) {
|
|
144
|
+
vitals.fcp = {
|
|
145
|
+
value: now(),
|
|
146
|
+
loadState: globalScope.document?.readyState || null
|
|
147
|
+
}
|
|
148
|
+
obs.disconnect()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Track LCP - largest contentful paint
|
|
153
|
+
let largestSize = 0
|
|
154
|
+
const lcpObs = observeMutations(id, (node) => {
|
|
155
|
+
try {
|
|
156
|
+
const elem = node.nodeType === 1 ? node : node.parentElement
|
|
157
|
+
if (!elem) return
|
|
158
|
+
const rect = elem.getBoundingClientRect()
|
|
159
|
+
const size = rect.width * rect.height
|
|
160
|
+
if (size > largestSize) {
|
|
161
|
+
largestSize = size
|
|
162
|
+
vitals.lcp = {
|
|
163
|
+
value: now(),
|
|
164
|
+
size,
|
|
165
|
+
elTag: elem.tagName || null,
|
|
166
|
+
eid: elem.id || null,
|
|
167
|
+
elUrl: getElementURL(elem)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
// Element may be detached from DOM
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
observers.push(lcpObs)
|
|
175
|
+
|
|
176
|
+
// Track CLS - cumulative layout shift
|
|
177
|
+
// Initialize CLS to 0 if browser supports it AND the MFE container exists in the DOM
|
|
178
|
+
let clsValue = 0
|
|
179
|
+
const clsObs = observePerformance(observers, { type: 'layout-shift', buffered: true }, (entry) => {
|
|
180
|
+
if (entry.hadRecentInput || !vitals.cls) return
|
|
181
|
+
(entry.sources || []).some(source => {
|
|
182
|
+
if (isInMFE(source.node, id)) {
|
|
183
|
+
clsValue += entry.value
|
|
184
|
+
vitals.cls ??= { value: 0, largestShiftValue: null, largestShiftTime: null, largestShiftTarget: null, loadState: null }
|
|
185
|
+
vitals.cls.value = clsValue
|
|
186
|
+
|
|
187
|
+
// Track largest shift
|
|
188
|
+
if (entry.value > (vitals.cls.largestShiftValue || 0)) {
|
|
189
|
+
vitals.cls.largestShiftValue = entry.value
|
|
190
|
+
vitals.cls.largestShiftTime = entry.startTime
|
|
191
|
+
vitals.cls.largestShiftTarget = getElementSelector(source.node)
|
|
192
|
+
vitals.cls.loadState = globalScope.document?.readyState || null
|
|
193
|
+
}
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
return false
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
if (clsObs) {
|
|
201
|
+
try {
|
|
202
|
+
const mfeContainer = globalScope.document?.querySelector(`[data-nr-mfe-id="${id}"]`)
|
|
203
|
+
if (mfeContainer) {
|
|
204
|
+
vitals.cls ??= { value: 0, largestShiftValue: null, largestShiftTime: null, largestShiftTarget: null, loadState: null }
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// querySelector may fail, leave CLS as null
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Track INP - interaction to next paint
|
|
212
|
+
observePerformance(observers, { type: 'event', buffered: true, durationThreshold: 16 }, (entry) => {
|
|
213
|
+
if (!entry.interactionId || !entry.target || !isInMFE(entry.target, id)) return
|
|
214
|
+
if (vitals.inp === null || entry.duration > vitals.inp.value) {
|
|
215
|
+
vitals.inp = {
|
|
216
|
+
value: entry.duration,
|
|
217
|
+
interactionTarget: getElementSelector(entry.target),
|
|
218
|
+
interactionTime: entry.startTime,
|
|
219
|
+
interactionType: entry.name,
|
|
220
|
+
inputDelay: entry.processingStart ? entry.processingStart - entry.startTime : null,
|
|
221
|
+
processingDuration: (entry.processingStart && entry.processingEnd) ? entry.processingEnd - entry.processingStart : null,
|
|
222
|
+
presentationDelay: (entry.processingEnd && entry.duration) ? entry.duration - (entry.processingEnd - entry.startTime) : null,
|
|
223
|
+
nextPaintTime: entry.startTime + entry.duration,
|
|
224
|
+
loadState: globalScope.document?.readyState || null
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// Disconnect all observers
|
|
230
|
+
vitals.disconnect = () => {
|
|
231
|
+
// Disconnect all observers
|
|
232
|
+
observers.forEach(obs => {
|
|
233
|
+
try {
|
|
234
|
+
obs?.disconnect()
|
|
235
|
+
} catch (e) {
|
|
236
|
+
// Observer may already be disconnected
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
|
|
242
|
+
// CLS and INP continue tracking until visibility change or deregister
|
|
243
|
+
const disconnectLCP = () => {
|
|
244
|
+
try {
|
|
245
|
+
lcpObs?.disconnect()
|
|
246
|
+
} catch (e) {
|
|
247
|
+
// Observer may already be disconnected
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
;['click', 'keydown', 'scroll'].forEach(type => {
|
|
251
|
+
globalScope.addEventListener(type, disconnectLCP, { once: true, passive: true })
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Disconnect all observers on visibility change or page unload
|
|
255
|
+
;['visibilitychange', 'pagehide'].forEach(type => {
|
|
256
|
+
globalScope.addEventListener(type, vitals.disconnect, { once: true, passive: true })
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
return vitals
|
|
260
|
+
}
|
|
@@ -8,6 +8,7 @@ import { now } from '../timing/now'
|
|
|
8
8
|
import { cleanURL } from '../url/clean-url'
|
|
9
9
|
import { gosNREUMOriginals } from '../window/nreum'
|
|
10
10
|
import { subscribeToPageUnload } from '../window/page-visibility'
|
|
11
|
+
import { EVENT_TYPES } from '../constants/events'
|
|
11
12
|
|
|
12
13
|
const wrapped = {}
|
|
13
14
|
const openWebSockets = new Set() // track all instances to close out metrics on page unload
|
|
@@ -37,7 +38,7 @@ export function wrapWebSocket (sharedEE) {
|
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
class WrappedWebSocket extends WebSocket {
|
|
40
|
-
static name =
|
|
41
|
+
static name = EVENT_TYPES.WS
|
|
41
42
|
static toString () { // fake native WebSocket when static class is stringified
|
|
42
43
|
return 'function WebSocket() { [native code] }'
|
|
43
44
|
}
|
|
@@ -17,7 +17,7 @@ import { warn } from '../util/console'
|
|
|
17
17
|
import { findTargetsFromStackTrace } from '../v2/utils'
|
|
18
18
|
|
|
19
19
|
const wrapped = {}
|
|
20
|
-
const XHR_PROPS = ['open', 'send'] // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
|
|
20
|
+
const XHR_PROPS = ['open', 'send', 'setRequestHeader'] // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Wraps the native XMLHttpRequest (XHR) object to emit custom events to its readystatechange event and an assortment
|
|
@@ -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 { isPureObject } from '../../../common/util/type-check'
|
|
@@ -97,3 +97,44 @@ function parseGQLQueryString (gqlQueryString) {
|
|
|
97
97
|
function validateGQLObject (obj) {
|
|
98
98
|
return !(typeof obj !== 'object' || !obj.query || typeof obj.query !== 'string')
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Checks if a response object has valid GraphQL errors.
|
|
103
|
+
* @param {object} response - A single GraphQL response object
|
|
104
|
+
* @returns {boolean} True if the response has valid errors
|
|
105
|
+
*/
|
|
106
|
+
function hasValidGQLErrors (response) {
|
|
107
|
+
return Array.isArray(response?.errors) &&
|
|
108
|
+
response.errors.some(err =>
|
|
109
|
+
err && typeof err === 'object' && typeof err.message === 'string'
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Checks if a response body contains GraphQL errors according to the GraphQL spec.
|
|
115
|
+
* A valid GraphQL error response contains an "errors" array with at least one error object.
|
|
116
|
+
* Supports both single and batched GraphQL responses.
|
|
117
|
+
* @param {string|object|array} [responseBody] The response body to check
|
|
118
|
+
* @returns {boolean} True if the response contains GraphQL errors
|
|
119
|
+
*/
|
|
120
|
+
export function hasGQLErrors (responseBody) {
|
|
121
|
+
if (!responseBody) return false
|
|
122
|
+
try {
|
|
123
|
+
let parsed = responseBody
|
|
124
|
+
|
|
125
|
+
// Parse string to object if needed
|
|
126
|
+
if (typeof responseBody === 'string') {
|
|
127
|
+
parsed = JSON.parse(responseBody)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Handle batched GraphQL responses (array of response objects)
|
|
131
|
+
if (Array.isArray(parsed)) {
|
|
132
|
+
return parsed.some(hasValidGQLErrors)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Handle single GraphQL response
|
|
136
|
+
return hasValidGQLErrors(parsed)
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -6,13 +6,16 @@ import { registerHandler } from '../../../common/event-emitter/register-handler'
|
|
|
6
6
|
import { stringify } from '../../../common/util/stringify'
|
|
7
7
|
import { handle } from '../../../common/event-emitter/handle'
|
|
8
8
|
import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list'
|
|
9
|
-
import {
|
|
9
|
+
import { FEATURE_NAME, AJAX_ID } from '../constants'
|
|
10
10
|
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
11
11
|
import { AggregateBase } from '../../utils/aggregate-base'
|
|
12
|
-
import { parseGQL } from './gql'
|
|
13
12
|
import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
|
|
14
13
|
import { gosNREUMOriginals } from '../../../common/window/nreum'
|
|
14
|
+
import { hasGQLErrors, parseGQL } from './gql'
|
|
15
|
+
import { canCapturePayload, isLikelyHumanReadable, parseQueryString, createStringAdders } from '../../../common/payloads/payloads'
|
|
16
|
+
import { Obfuscator } from '../../../common/util/obfuscate'
|
|
15
17
|
import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils'
|
|
18
|
+
import { EVENT_TYPES } from '../../../common/constants/events'
|
|
16
19
|
import { generateUuid } from '../../../common/ids/unique-id'
|
|
17
20
|
|
|
18
21
|
export class Aggregate extends AggregateBase {
|
|
@@ -23,6 +26,9 @@ export class Aggregate extends AggregateBase {
|
|
|
23
26
|
setDenyList(agentRef.runtime.denyList)
|
|
24
27
|
const classThis = this
|
|
25
28
|
|
|
29
|
+
// Create obfuscator for AJAX requests
|
|
30
|
+
this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.AJAX)
|
|
31
|
+
|
|
26
32
|
if (!agentRef.init.ajax.block_internal) {
|
|
27
33
|
// if the agent is tracking ITSELF, it can spawn endless ajax requests early if they are large from custom attributes, so we just disable early harvest for ajax in this case.
|
|
28
34
|
super.canHarvestEarly = false
|
|
@@ -96,6 +102,24 @@ export class Aggregate extends AggregateBase {
|
|
|
96
102
|
[AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
event.gql = params.gql = parseGQL({
|
|
106
|
+
body: ctx.requestBody,
|
|
107
|
+
query: ctx.parsedOrigin?.search
|
|
108
|
+
})
|
|
109
|
+
if (event.gql) event.gql.operationHasErrors = params.gql.operationHasErrors = hasGQLErrors(ctx.responseBody)
|
|
110
|
+
|
|
111
|
+
const capturePayloadSetting = this.agentRef.init.ajax.capture_payloads
|
|
112
|
+
const shouldCapturePayload = canCapturePayload(capturePayloadSetting, params.status, event.gql?.operationHasErrors)
|
|
113
|
+
|
|
114
|
+
if (shouldCapturePayload) {
|
|
115
|
+
// Store raw data; obfuscation and truncation will happen in the serializer
|
|
116
|
+
params.requestQuery = event.requestQuery = parseQueryString(ctx.parsedOrigin?.search)
|
|
117
|
+
params.requestHeaders = event.requestHeaders = ctx.requestHeaders
|
|
118
|
+
params.responseHeaders = event.responseHeaders = ctx.responseHeaders
|
|
119
|
+
if (isLikelyHumanReadable(ctx.requestHeaders, ctx.requestBody)) params.requestBody = event.requestBody = ctx.requestBody
|
|
120
|
+
if (isLikelyHumanReadable(ctx.responseHeaders, ctx.responseBody)) params.responseBody = event.responseBody = ctx.responseBody
|
|
121
|
+
}
|
|
122
|
+
|
|
99
123
|
if (ctx.dt) {
|
|
100
124
|
event.spanId = ctx.dt.spanId
|
|
101
125
|
event.traceId = ctx.dt.traceId
|
|
@@ -104,11 +128,6 @@ export class Aggregate extends AggregateBase {
|
|
|
104
128
|
)
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
// parsed from the AJAX body, looking for operationName param & parsing query for operationType
|
|
108
|
-
event.gql = params.gql = parseGQL({
|
|
109
|
-
body: ctx.body,
|
|
110
|
-
query: ctx.parsedOrigin?.search
|
|
111
|
-
})
|
|
112
131
|
if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length)
|
|
113
132
|
|
|
114
133
|
/** make a copy of the event for the MFE target if it exists */
|
|
@@ -131,7 +150,9 @@ export class Aggregate extends AggregateBase {
|
|
|
131
150
|
|
|
132
151
|
serializer (eventBuffer) {
|
|
133
152
|
if (!eventBuffer.length) return
|
|
134
|
-
|
|
153
|
+
|
|
154
|
+
const { addString, addStringWithTruncation } = createStringAdders(getAddStringContext, this.obfuscator)
|
|
155
|
+
|
|
135
156
|
let payload = 'bel.7;'
|
|
136
157
|
|
|
137
158
|
let firstTimestamp = 0
|
|
@@ -165,15 +186,24 @@ export class Aggregate extends AggregateBase {
|
|
|
165
186
|
// Since configuration objects (like info) are created new each time they are set, we have to grab the current pointer to the attr object here.
|
|
166
187
|
const jsAttributes = this.agentRef.info.jsAttributes
|
|
167
188
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
const attrParts = addCustomAttributes({
|
|
189
|
+
// Regular attributes: obfuscate only
|
|
190
|
+
const regularAttrs = addCustomAttributes({
|
|
171
191
|
...(jsAttributes || {}),
|
|
172
|
-
|
|
192
|
+
[AJAX_ID]: event[AJAX_ID], // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
173
193
|
...(event.targetAttributes || {}), // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
|
|
174
|
-
|
|
194
|
+
...(event.gql || {})
|
|
175
195
|
}, addString)
|
|
176
196
|
|
|
197
|
+
// Payload attributes: obfuscate then truncate
|
|
198
|
+
const payloadAttrs = addCustomAttributes({
|
|
199
|
+
...(event.requestBody ? { requestBody: event.requestBody } : {}),
|
|
200
|
+
...(event.requestHeaders ? { requestHeaders: event.requestHeaders } : {}),
|
|
201
|
+
...(event.requestQuery ? { requestQuery: event.requestQuery } : {}),
|
|
202
|
+
...(event.responseBody ? { responseBody: event.responseBody } : {}),
|
|
203
|
+
...(event.responseHeaders ? { responseHeaders: event.responseHeaders } : {})
|
|
204
|
+
}, addStringWithTruncation)
|
|
205
|
+
|
|
206
|
+
const attrParts = [...regularAttrs, ...payloadAttrs]
|
|
177
207
|
fields.unshift(numeric(attrParts.length))
|
|
178
208
|
|
|
179
209
|
insert += fields.join(',')
|
|
@@ -5,5 +5,9 @@
|
|
|
5
5
|
import { FEATURE_NAMES } from '../../loaders/features/features'
|
|
6
6
|
|
|
7
7
|
export const FEATURE_NAME = FEATURE_NAMES.ajax
|
|
8
|
-
|
|
8
|
+
export const CAPTURE_PAYLOAD_SETTINGS = {
|
|
9
|
+
NONE: 'none',
|
|
10
|
+
FAILURES: 'failures',
|
|
11
|
+
ALL: 'all'
|
|
12
|
+
}
|
|
9
13
|
export const AJAX_ID = 'ajaxRequest.id'
|
|
@@ -14,7 +14,7 @@ import { parseUrl } from '../../../common/url/parse-url'
|
|
|
14
14
|
import { DT } from './distributed-tracing'
|
|
15
15
|
import { responseSizeFromXhr } from './response-size'
|
|
16
16
|
import { InstrumentBase } from '../../utils/instrument-base'
|
|
17
|
-
import { FEATURE_NAME } from '../constants'
|
|
17
|
+
import { CAPTURE_PAYLOAD_SETTINGS, FEATURE_NAME } from '../constants'
|
|
18
18
|
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
19
19
|
import { SUPPORTABILITY_METRIC } from '../../metrics/constants'
|
|
20
20
|
import { now } from '../../../common/timing/now'
|
|
@@ -27,6 +27,7 @@ var handlersLen = handlers.length
|
|
|
27
27
|
var origRequest = gosNREUMOriginals().o.REQ
|
|
28
28
|
var origXHR = gosNREUMOriginals().o.XHR
|
|
29
29
|
const NR_CAT_HEADER = 'X-NewRelic-App-Data'
|
|
30
|
+
const INTERNAL_ERROR = 'internal-error'
|
|
30
31
|
|
|
31
32
|
export class Instrument extends InstrumentBase {
|
|
32
33
|
static featureName = FEATURE_NAME
|
|
@@ -62,16 +63,18 @@ export class Instrument extends InstrumentBase {
|
|
|
62
63
|
wrapFetch(this.ee, agentRef)
|
|
63
64
|
wrapXhr(this.ee, agentRef)
|
|
64
65
|
subscribeToEvents(agentRef, this.ee, this.handler, this.dt)
|
|
65
|
-
|
|
66
66
|
this.importAggregator(agentRef, () => import(/* webpackChunkName: "ajax-aggregate" */ '../aggregate/index.js'))
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
71
|
+
const shouldInterceptPayloads = [CAPTURE_PAYLOAD_SETTINGS.ALL, CAPTURE_PAYLOAD_SETTINGS.FAILURES].includes(agentRef.init.ajax?.capture_payloads)
|
|
72
|
+
|
|
71
73
|
ee.on('new-xhr', onNewXhr)
|
|
72
74
|
ee.on('open-xhr-start', onOpenXhrStart)
|
|
73
75
|
ee.on('open-xhr-end', onOpenXhrEnd)
|
|
74
76
|
ee.on('send-xhr-start', onSendXhrStart)
|
|
77
|
+
ee.on('setRequestHeader-xhr-start', onSetRequestHeader)
|
|
75
78
|
ee.on('xhr-cb-time', onXhrCbTime)
|
|
76
79
|
ee.on('xhr-load-added', onXhrLoadAdded)
|
|
77
80
|
ee.on('xhr-load-removed', onXhrLoadRemoved)
|
|
@@ -147,6 +150,14 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
function onSetRequestHeader (args, xhr) {
|
|
154
|
+
// args[0] = header name, args[1] = header value
|
|
155
|
+
if (shouldInterceptPayloads && args.length >= 2) {
|
|
156
|
+
this.requestHeaders ??= {}
|
|
157
|
+
this.requestHeaders[args[0].toLowerCase()] = args[1]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
function onSendXhrStart (args, xhr) {
|
|
151
162
|
var metrics = this.metrics
|
|
152
163
|
var data = args[0]
|
|
@@ -159,7 +170,7 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
159
170
|
|
|
160
171
|
this.startTime = now()
|
|
161
172
|
|
|
162
|
-
this.
|
|
173
|
+
this.requestBody = data
|
|
163
174
|
|
|
164
175
|
this.listener = function (evt) {
|
|
165
176
|
try {
|
|
@@ -169,7 +180,7 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
169
180
|
if (evt.type !== 'load' || ((context.called === context.totalCbs) && (context.onloadCalled || typeof (xhr.onload) !== 'function') && typeof context.end === 'function')) context.end(xhr)
|
|
170
181
|
} catch (e) {
|
|
171
182
|
try {
|
|
172
|
-
ee.emit(
|
|
183
|
+
ee.emit(INTERNAL_ERROR, [e])
|
|
173
184
|
} catch (err) {
|
|
174
185
|
// do nothing
|
|
175
186
|
}
|
|
@@ -321,9 +332,29 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
321
332
|
const method = ('' + ((target && target instanceof origRequest && target.method) ||
|
|
322
333
|
opts.method || 'GET')).toUpperCase()
|
|
323
334
|
this.params.method = method
|
|
324
|
-
this.body = opts.body
|
|
325
335
|
|
|
326
|
-
this.txSize = dataSize(opts.body) || 0
|
|
336
|
+
this.txSize = dataSize(opts.body || (target?.body)) || 0
|
|
337
|
+
|
|
338
|
+
// Capture request headers
|
|
339
|
+
try {
|
|
340
|
+
var headers = opts.headers || (target?.headers)
|
|
341
|
+
if (shouldInterceptPayloads && headers) {
|
|
342
|
+
this.requestHeaders ??= {}
|
|
343
|
+
if (headers instanceof Headers) {
|
|
344
|
+
headers.forEach(function (value, key) {
|
|
345
|
+
this.requestHeaders[key.toLowerCase()] = value
|
|
346
|
+
}.bind(this))
|
|
347
|
+
} else if (typeof headers === 'object') {
|
|
348
|
+
for (var key in headers) {
|
|
349
|
+
this.requestHeaders[key.toLowerCase()] = headers[key]
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
// Silently fail if we can't access headers
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.requestBody = opts.body || (target?.body)
|
|
327
358
|
}
|
|
328
359
|
|
|
329
360
|
// we capture failed call as status 0, the actual error is ignored
|
|
@@ -335,20 +366,47 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
335
366
|
|
|
336
367
|
this.params.status = res ? res.status : 0
|
|
337
368
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
responseSize =
|
|
369
|
+
const finishAndReport = () => {
|
|
370
|
+
// convert rxSize to a number - handle both string (from content-length header) and number (from fallback)
|
|
371
|
+
const num = +this.rxSize
|
|
372
|
+
const responseSize = this.rxSize != null && !isNaN(num) ? num : undefined
|
|
373
|
+
|
|
374
|
+
const metrics = {
|
|
375
|
+
txSize: this.txSize,
|
|
376
|
+
rxSize: responseSize,
|
|
377
|
+
duration: this.endTime - this.startTime
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const payload = [this.params, metrics, this.startTime, this.endTime, 'fetch']
|
|
381
|
+
this.targets.forEach(target => reportToAgg(payload, this, target))
|
|
342
382
|
}
|
|
343
383
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
384
|
+
/** Since accessing fetch bodies is an async process, these are
|
|
385
|
+
* reasonable conditions to check to not do needless extra work */
|
|
386
|
+
if (!res || !shouldInterceptPayloads) {
|
|
387
|
+
finishAndReport()
|
|
388
|
+
return
|
|
348
389
|
}
|
|
349
390
|
|
|
350
|
-
|
|
351
|
-
|
|
391
|
+
// Clone the response to read the body without consuming the original
|
|
392
|
+
res.clone().text().then((text) => {
|
|
393
|
+
this.responseBody = text
|
|
394
|
+
// Use captured payload size as fallback if content-length header was missing or is 0 with a body
|
|
395
|
+
// Only apply fallback for non-network-error responses (status !== 0)
|
|
396
|
+
if ((!this.rxSize || this.rxSize === '0' || this.rxSize === 0) && text !== undefined && this.params.status !== 0) {
|
|
397
|
+
this.rxSize = dataSize(text)
|
|
398
|
+
}
|
|
399
|
+
if (res?.headers) {
|
|
400
|
+
this.responseHeaders = {}
|
|
401
|
+
res.headers.forEach(function (value, key) {
|
|
402
|
+
this.responseHeaders[key.toLowerCase()] = value
|
|
403
|
+
}.bind(this))
|
|
404
|
+
}
|
|
405
|
+
}).catch((err) => {
|
|
406
|
+
ee.emit(INTERNAL_ERROR, [err])
|
|
407
|
+
}).finally(() => {
|
|
408
|
+
finishAndReport()
|
|
409
|
+
})
|
|
352
410
|
}
|
|
353
411
|
|
|
354
412
|
// Create report for XHR request that has finished
|
|
@@ -365,7 +423,7 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
365
423
|
if (params.aborted) return
|
|
366
424
|
if (hasUndefinedHostname(params)) return // don't bother with XHR of url with no hostname
|
|
367
425
|
|
|
368
|
-
metrics.duration =
|
|
426
|
+
metrics.duration = this.endTime - this.startTime
|
|
369
427
|
if (!this.loadCaptureCalled && xhr.readyState === 4) {
|
|
370
428
|
captureXhrData(this, xhr)
|
|
371
429
|
} else if (params.status == null) {
|
|
@@ -375,6 +433,27 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
375
433
|
// Always send cbTime, even if no noticeable time was taken.
|
|
376
434
|
metrics.cbTime = this.cbTime
|
|
377
435
|
|
|
436
|
+
if (shouldInterceptPayloads) {
|
|
437
|
+
try {
|
|
438
|
+
this.responseBody = xhr.responseText
|
|
439
|
+
} catch (e) {
|
|
440
|
+
this.responseBody = xhr.response
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Use captured payload size as fallback if not already determined or is 0 with a body
|
|
444
|
+
// Only apply fallback for non-network-error responses (status !== 0)
|
|
445
|
+
if ((!metrics.rxSize || metrics.rxSize === 0) && this.responseBody !== undefined && params.status !== 0) {
|
|
446
|
+
const size = dataSize(this.responseBody)
|
|
447
|
+
if (size !== undefined) metrics.rxSize = size
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
this.responseHeaders = parseResponseHeaders(xhr.getAllResponseHeaders())
|
|
452
|
+
} catch (err) {
|
|
453
|
+
ee.emit(INTERNAL_ERROR, [err])
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
378
457
|
const payload = [params, metrics, this.startTime, this.endTime, 'xhr']
|
|
379
458
|
this.targets.forEach(target => reportToAgg(payload, this, target))
|
|
380
459
|
}
|
|
@@ -387,7 +466,8 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
387
466
|
ctx.params.status = xhr.status
|
|
388
467
|
|
|
389
468
|
var size = responseSizeFromXhr(xhr, ctx.lastSize)
|
|
390
|
-
|
|
469
|
+
// Don't set rxSize for network errors (status 0)
|
|
470
|
+
if (size !== undefined && xhr.status !== 0) ctx.metrics.rxSize = size
|
|
391
471
|
|
|
392
472
|
if (ctx.sameOrigin && xhr.getAllResponseHeaders().indexOf(NR_CAT_HEADER) >= 0) {
|
|
393
473
|
var header = xhr.getResponseHeader(NR_CAT_HEADER)
|
|
@@ -414,4 +494,20 @@ function addUrl (ctx, url) {
|
|
|
414
494
|
ctx.sameOrigin = parsed.sameOrigin
|
|
415
495
|
}
|
|
416
496
|
|
|
497
|
+
function parseResponseHeaders (headerStr) {
|
|
498
|
+
const headers = {}
|
|
499
|
+
if (!headerStr) return headers
|
|
500
|
+
|
|
501
|
+
headerStr.split('\r\n').forEach(function (line) {
|
|
502
|
+
const separatorIndex = line.indexOf(': ')
|
|
503
|
+
if (separatorIndex > 0) {
|
|
504
|
+
const name = line.substring(0, separatorIndex)
|
|
505
|
+
const value = line.substring(separatorIndex + 2)
|
|
506
|
+
headers[name.toLowerCase()] = value
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
return headers
|
|
511
|
+
}
|
|
512
|
+
|
|
417
513
|
export const Ajax = Instrument
|