@newrelic/browser-agent 1.316.0 → 1.317.0-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 +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,281 @@
|
|
|
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
|
+
let selector = elem.tagName.toLowerCase();
|
|
23
|
+
if (elem.id) selector += "#".concat(elem.id);else if (elem.className && typeof elem.className === 'string') {
|
|
24
|
+
const classes = elem.className.trim().split(/\s+/).slice(0, 2).join('.');
|
|
25
|
+
if (classes) selector += ".".concat(classes);
|
|
26
|
+
}
|
|
27
|
+
return selector;
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract URL from an element (for images, videos, etc.)
|
|
35
|
+
* @param {Element} elem - The element to extract URL from
|
|
36
|
+
* @returns {string|null} Cleaned URL or null
|
|
37
|
+
*/
|
|
38
|
+
const getElementURL = elem => {
|
|
39
|
+
try {
|
|
40
|
+
if (!elem) return null;
|
|
41
|
+
|
|
42
|
+
// For images and videos
|
|
43
|
+
if (elem.src) return cleanURL(elem.src);
|
|
44
|
+
|
|
45
|
+
// For elements with background images
|
|
46
|
+
const bgImage = globalScope.getComputedStyle?.(elem)?.backgroundImage;
|
|
47
|
+
if (bgImage && bgImage !== 'none') {
|
|
48
|
+
const match = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
|
|
49
|
+
if (match?.[1]) return cleanURL(match[1]);
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const isObservable = node => {
|
|
57
|
+
try {
|
|
58
|
+
return node?.textContent?.trim() || ['img', 'video', 'canvas', 'svg'].includes(node?.nodeName?.toLowerCase());
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if node is within a specific MFE
|
|
66
|
+
* @param {Node} node - DOM node to check
|
|
67
|
+
* @param {string} id - MFE ID to match
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
const isInMFE = (node, id) => {
|
|
71
|
+
try {
|
|
72
|
+
let curr = node.nodeType === 1 ? node : node.parentElement;
|
|
73
|
+
while (curr?.tagName) {
|
|
74
|
+
if (curr.dataset?.nrMfeId === id) return true;
|
|
75
|
+
curr = curr.parentNode;
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {}
|
|
78
|
+
return false;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create mutation observer for MFE nodes
|
|
83
|
+
* @param {string} id - MFE ID to track
|
|
84
|
+
* @param {Function} onMatch - Callback when matching node is added
|
|
85
|
+
* @returns {MutationObserver}
|
|
86
|
+
*/
|
|
87
|
+
const observeMutations = (id, onMatch) => {
|
|
88
|
+
const obs = new globalScope.MutationObserver(mutations => {
|
|
89
|
+
mutations.forEach(m => m.addedNodes.forEach(node => {
|
|
90
|
+
if (isObservable(node) && isInMFE(node, id)) {
|
|
91
|
+
onMatch(node, obs);
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
});
|
|
95
|
+
obs.observe(globalScope.document, {
|
|
96
|
+
childList: true,
|
|
97
|
+
subtree: true
|
|
98
|
+
});
|
|
99
|
+
return obs;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create performance observer for MFE entries
|
|
104
|
+
* @param {Array} observers - Array to track observers
|
|
105
|
+
* @param {Object} config - Observer configuration
|
|
106
|
+
* @param {Function} onEntry - Callback for each entry
|
|
107
|
+
* @returns {PerformanceObserver|null} Observer if successful, null if unsupported
|
|
108
|
+
*/
|
|
109
|
+
const observePerformance = (observers, config, onEntry) => {
|
|
110
|
+
try {
|
|
111
|
+
const obs = new globalScope.PerformanceObserver(list => {
|
|
112
|
+
list.getEntries().forEach(onEntry);
|
|
113
|
+
});
|
|
114
|
+
obs.observe(config);
|
|
115
|
+
observers.push(obs);
|
|
116
|
+
return obs;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Tracks all Core Web Vitals for a specific MFE.
|
|
124
|
+
* @param {string} id - The MFE ID to track
|
|
125
|
+
* @returns {{fcp: object|null, lcp: object|null, cls: object|null, inp: object|null, disconnect: Function}}
|
|
126
|
+
*/
|
|
127
|
+
export function trackMFEVitals(id) {
|
|
128
|
+
const vitals = {
|
|
129
|
+
fcp: null,
|
|
130
|
+
lcp: null,
|
|
131
|
+
cls: null,
|
|
132
|
+
inp: null,
|
|
133
|
+
disconnect: () => {}
|
|
134
|
+
};
|
|
135
|
+
if (!id || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals;
|
|
136
|
+
const observers = [];
|
|
137
|
+
|
|
138
|
+
// Track FCP - first contentful paint
|
|
139
|
+
observeMutations(id, (node, obs) => {
|
|
140
|
+
if (vitals.fcp === null) {
|
|
141
|
+
vitals.fcp = {
|
|
142
|
+
value: now(),
|
|
143
|
+
loadState: globalScope.document?.readyState || null
|
|
144
|
+
};
|
|
145
|
+
obs.disconnect();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Track LCP - largest contentful paint
|
|
150
|
+
let largestSize = 0;
|
|
151
|
+
const lcpObs = observeMutations(id, node => {
|
|
152
|
+
try {
|
|
153
|
+
const elem = node.nodeType === 1 ? node : node.parentElement;
|
|
154
|
+
if (!elem) return;
|
|
155
|
+
const rect = elem.getBoundingClientRect();
|
|
156
|
+
const size = rect.width * rect.height;
|
|
157
|
+
if (size > largestSize) {
|
|
158
|
+
largestSize = size;
|
|
159
|
+
vitals.lcp = {
|
|
160
|
+
value: now(),
|
|
161
|
+
size,
|
|
162
|
+
elTag: elem.tagName || null,
|
|
163
|
+
eid: elem.id || null,
|
|
164
|
+
elUrl: getElementURL(elem)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
// Element may be detached from DOM
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
observers.push(lcpObs);
|
|
172
|
+
|
|
173
|
+
// Track CLS - cumulative layout shift
|
|
174
|
+
// Initialize CLS to 0 if browser supports it AND the MFE container exists in the DOM
|
|
175
|
+
let clsValue = 0;
|
|
176
|
+
const clsObs = observePerformance(observers, {
|
|
177
|
+
type: 'layout-shift',
|
|
178
|
+
buffered: true
|
|
179
|
+
}, 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 ??= {
|
|
185
|
+
value: 0,
|
|
186
|
+
largestShiftValue: null,
|
|
187
|
+
largestShiftTime: null,
|
|
188
|
+
largestShiftTarget: null,
|
|
189
|
+
loadState: null
|
|
190
|
+
};
|
|
191
|
+
vitals.cls.value = clsValue;
|
|
192
|
+
|
|
193
|
+
// Track largest shift
|
|
194
|
+
if (entry.value > (vitals.cls.largestShiftValue || 0)) {
|
|
195
|
+
vitals.cls.largestShiftValue = entry.value;
|
|
196
|
+
vitals.cls.largestShiftTime = entry.startTime;
|
|
197
|
+
vitals.cls.largestShiftTarget = getElementSelector(source.node);
|
|
198
|
+
vitals.cls.loadState = globalScope.document?.readyState || null;
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
if (clsObs) {
|
|
206
|
+
try {
|
|
207
|
+
const mfeContainer = globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"));
|
|
208
|
+
if (mfeContainer) {
|
|
209
|
+
vitals.cls ??= {
|
|
210
|
+
value: 0,
|
|
211
|
+
largestShiftValue: null,
|
|
212
|
+
largestShiftTime: null,
|
|
213
|
+
largestShiftTarget: null,
|
|
214
|
+
loadState: null
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// querySelector may fail, leave CLS as null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Track INP - interaction to next paint
|
|
223
|
+
observePerformance(observers, {
|
|
224
|
+
type: 'event',
|
|
225
|
+
buffered: true,
|
|
226
|
+
durationThreshold: 16
|
|
227
|
+
}, entry => {
|
|
228
|
+
if (!entry.interactionId || !entry.target || !isInMFE(entry.target, id)) return;
|
|
229
|
+
if (vitals.inp === null || entry.duration > vitals.inp.value) {
|
|
230
|
+
vitals.inp = {
|
|
231
|
+
value: entry.duration,
|
|
232
|
+
interactionTarget: getElementSelector(entry.target),
|
|
233
|
+
interactionTime: entry.startTime,
|
|
234
|
+
interactionType: entry.name,
|
|
235
|
+
inputDelay: entry.processingStart ? entry.processingStart - entry.startTime : null,
|
|
236
|
+
processingDuration: entry.processingStart && entry.processingEnd ? entry.processingEnd - entry.processingStart : null,
|
|
237
|
+
presentationDelay: entry.processingEnd && entry.duration ? entry.duration - (entry.processingEnd - entry.startTime) : null,
|
|
238
|
+
nextPaintTime: entry.startTime + entry.duration,
|
|
239
|
+
loadState: globalScope.document?.readyState || null
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Disconnect all observers
|
|
245
|
+
vitals.disconnect = () => {
|
|
246
|
+
// Disconnect all observers
|
|
247
|
+
observers.forEach(obs => {
|
|
248
|
+
try {
|
|
249
|
+
obs?.disconnect();
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// Observer may already be disconnected
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
|
|
257
|
+
// CLS and INP continue tracking until visibility change or deregister
|
|
258
|
+
const disconnectLCP = () => {
|
|
259
|
+
try {
|
|
260
|
+
lcpObs?.disconnect();
|
|
261
|
+
} catch (e) {
|
|
262
|
+
// Observer may already be disconnected
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
['click', 'keydown', 'scroll'].forEach(type => {
|
|
266
|
+
globalScope.addEventListener(type, disconnectLCP, {
|
|
267
|
+
once: true,
|
|
268
|
+
passive: true
|
|
269
|
+
});
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Disconnect all observers on visibility change or page unload
|
|
273
|
+
;
|
|
274
|
+
['visibilitychange', 'pagehide'].forEach(type => {
|
|
275
|
+
globalScope.addEventListener(type, vitals.disconnect, {
|
|
276
|
+
once: true,
|
|
277
|
+
passive: true
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
return vitals;
|
|
281
|
+
}
|
|
@@ -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
|
const wrapped = {};
|
|
12
13
|
const openWebSockets = new Set(); // track all instances to close out metrics on page unload
|
|
13
14
|
|
|
@@ -33,7 +34,7 @@ export function wrapWebSocket(sharedEE) {
|
|
|
33
34
|
});
|
|
34
35
|
});
|
|
35
36
|
class WrappedWebSocket extends WebSocket {
|
|
36
|
-
static name =
|
|
37
|
+
static name = EVENT_TYPES.WS;
|
|
37
38
|
static toString() {
|
|
38
39
|
// fake native WebSocket when static class is stringified
|
|
39
40
|
return 'function WebSocket() { [native code] }';
|
|
@@ -16,7 +16,7 @@ import { globalScope } from '../constants/runtime';
|
|
|
16
16
|
import { warn } from '../util/console';
|
|
17
17
|
import { findTargetsFromStackTrace } from '../v2/utils';
|
|
18
18
|
const wrapped = {};
|
|
19
|
-
const XHR_PROPS = ['open', 'send']; // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
|
|
19
|
+
const XHR_PROPS = ['open', 'send', 'setRequestHeader']; // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* 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';
|
|
@@ -89,4 +89,42 @@ function parseGQLQueryString(gqlQueryString) {
|
|
|
89
89
|
}
|
|
90
90
|
function validateGQLObject(obj) {
|
|
91
91
|
return !(typeof obj !== 'object' || !obj.query || typeof obj.query !== 'string');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Checks if a response object has valid GraphQL errors.
|
|
96
|
+
* @param {object} response - A single GraphQL response object
|
|
97
|
+
* @returns {boolean} True if the response has valid errors
|
|
98
|
+
*/
|
|
99
|
+
function hasValidGQLErrors(response) {
|
|
100
|
+
return Array.isArray(response?.errors) && response.errors.some(err => err && typeof err === 'object' && typeof err.message === 'string');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Checks if a response body contains GraphQL errors according to the GraphQL spec.
|
|
105
|
+
* A valid GraphQL error response contains an "errors" array with at least one error object.
|
|
106
|
+
* Supports both single and batched GraphQL responses.
|
|
107
|
+
* @param {string|object|array} [responseBody] The response body to check
|
|
108
|
+
* @returns {boolean} True if the response contains GraphQL errors
|
|
109
|
+
*/
|
|
110
|
+
export function hasGQLErrors(responseBody) {
|
|
111
|
+
if (!responseBody) return false;
|
|
112
|
+
try {
|
|
113
|
+
let parsed = responseBody;
|
|
114
|
+
|
|
115
|
+
// Parse string to object if needed
|
|
116
|
+
if (typeof responseBody === 'string') {
|
|
117
|
+
parsed = JSON.parse(responseBody);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle batched GraphQL responses (array of response objects)
|
|
121
|
+
if (Array.isArray(parsed)) {
|
|
122
|
+
return parsed.some(hasValidGQLErrors);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle single GraphQL response
|
|
126
|
+
return hasValidGQLErrors(parsed);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
92
130
|
}
|
|
@@ -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
|
export class Aggregate extends AggregateBase {
|
|
18
21
|
static featureName = FEATURE_NAME;
|
|
@@ -20,6 +23,9 @@ export class Aggregate extends AggregateBase {
|
|
|
20
23
|
super(agentRef, FEATURE_NAME);
|
|
21
24
|
setDenyList(agentRef.runtime.denyList);
|
|
22
25
|
const classThis = this;
|
|
26
|
+
|
|
27
|
+
// Create obfuscator for AJAX requests
|
|
28
|
+
this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.AJAX);
|
|
23
29
|
if (!agentRef.init.ajax.block_internal) {
|
|
24
30
|
// 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.
|
|
25
31
|
super.canHarvestEarly = false;
|
|
@@ -84,17 +90,26 @@ export class Aggregate extends AggregateBase {
|
|
|
84
90
|
callbackDuration: metrics.cbTime,
|
|
85
91
|
[AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
86
92
|
};
|
|
93
|
+
event.gql = params.gql = parseGQL({
|
|
94
|
+
body: ctx.requestBody,
|
|
95
|
+
query: ctx.parsedOrigin?.search
|
|
96
|
+
});
|
|
97
|
+
if (event.gql) event.gql.operationHasErrors = params.gql.operationHasErrors = hasGQLErrors(ctx.responseBody);
|
|
98
|
+
const capturePayloadSetting = this.agentRef.init.ajax.capture_payloads;
|
|
99
|
+
const shouldCapturePayload = canCapturePayload(capturePayloadSetting, params.status, event.gql?.operationHasErrors);
|
|
100
|
+
if (shouldCapturePayload) {
|
|
101
|
+
// Store raw data; obfuscation and truncation will happen in the serializer
|
|
102
|
+
params.requestQuery = event.requestQuery = parseQueryString(ctx.parsedOrigin?.search);
|
|
103
|
+
params.requestHeaders = event.requestHeaders = ctx.requestHeaders;
|
|
104
|
+
params.responseHeaders = event.responseHeaders = ctx.responseHeaders;
|
|
105
|
+
if (isLikelyHumanReadable(ctx.requestHeaders, ctx.requestBody)) params.requestBody = event.requestBody = ctx.requestBody;
|
|
106
|
+
if (isLikelyHumanReadable(ctx.responseHeaders, ctx.responseBody)) params.responseBody = event.responseBody = ctx.responseBody;
|
|
107
|
+
}
|
|
87
108
|
if (ctx.dt) {
|
|
88
109
|
event.spanId = ctx.dt.spanId;
|
|
89
110
|
event.traceId = ctx.dt.traceId;
|
|
90
111
|
event.spanTimestamp = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp));
|
|
91
112
|
}
|
|
92
|
-
|
|
93
|
-
// parsed from the AJAX body, looking for operationName param & parsing query for operationType
|
|
94
|
-
event.gql = params.gql = parseGQL({
|
|
95
|
-
body: ctx.body,
|
|
96
|
-
query: ctx.parsedOrigin?.search
|
|
97
|
-
});
|
|
98
113
|
if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length);
|
|
99
114
|
|
|
100
115
|
/** make a copy of the event for the MFE target if it exists */
|
|
@@ -122,7 +137,10 @@ export class Aggregate extends AggregateBase {
|
|
|
122
137
|
}
|
|
123
138
|
serializer(eventBuffer) {
|
|
124
139
|
if (!eventBuffer.length) return;
|
|
125
|
-
const
|
|
140
|
+
const {
|
|
141
|
+
addString,
|
|
142
|
+
addStringWithTruncation
|
|
143
|
+
} = createStringAdders(getAddStringContext, this.obfuscator);
|
|
126
144
|
let payload = 'bel.7;';
|
|
127
145
|
let firstTimestamp = 0;
|
|
128
146
|
for (let i = 0; i < eventBuffer.length; i++) {
|
|
@@ -147,15 +165,35 @@ export class Aggregate extends AggregateBase {
|
|
|
147
165
|
// 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.
|
|
148
166
|
const jsAttributes = this.agentRef.info.jsAttributes;
|
|
149
167
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
const attrParts = addCustomAttributes({
|
|
168
|
+
// Regular attributes: obfuscate only
|
|
169
|
+
const regularAttrs = addCustomAttributes({
|
|
153
170
|
...(jsAttributes || {}),
|
|
154
|
-
|
|
171
|
+
[AJAX_ID]: event[AJAX_ID],
|
|
172
|
+
// all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
155
173
|
...(event.targetAttributes || {}),
|
|
156
174
|
// used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
|
|
157
|
-
|
|
175
|
+
...(event.gql || {})
|
|
158
176
|
}, addString);
|
|
177
|
+
|
|
178
|
+
// Payload attributes: obfuscate then truncate
|
|
179
|
+
const payloadAttrs = addCustomAttributes({
|
|
180
|
+
...(event.requestBody ? {
|
|
181
|
+
requestBody: event.requestBody
|
|
182
|
+
} : {}),
|
|
183
|
+
...(event.requestHeaders ? {
|
|
184
|
+
requestHeaders: event.requestHeaders
|
|
185
|
+
} : {}),
|
|
186
|
+
...(event.requestQuery ? {
|
|
187
|
+
requestQuery: event.requestQuery
|
|
188
|
+
} : {}),
|
|
189
|
+
...(event.responseBody ? {
|
|
190
|
+
responseBody: event.responseBody
|
|
191
|
+
} : {}),
|
|
192
|
+
...(event.responseHeaders ? {
|
|
193
|
+
responseHeaders: event.responseHeaders
|
|
194
|
+
} : {})
|
|
195
|
+
}, addStringWithTruncation);
|
|
196
|
+
const attrParts = [...regularAttrs, ...payloadAttrs];
|
|
159
197
|
fields.unshift(numeric(attrParts.length));
|
|
160
198
|
insert += fields.join(',');
|
|
161
199
|
if (attrParts && attrParts.length > 0) {
|
|
@@ -4,4 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { FEATURE_NAMES } from '../../loaders/features/features';
|
|
6
6
|
export const FEATURE_NAME = FEATURE_NAMES.ajax;
|
|
7
|
+
export const CAPTURE_PAYLOAD_SETTINGS = {
|
|
8
|
+
NONE: 'none',
|
|
9
|
+
FAILURES: 'failures',
|
|
10
|
+
ALL: 'all'
|
|
11
|
+
};
|
|
7
12
|
export const AJAX_ID = 'ajaxRequest.id';
|