@newrelic/browser-agent 1.317.0 → 1.318.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/common/config/configurable.js +1 -1
  3. package/dist/cjs/common/config/init-types.js +2 -0
  4. package/dist/cjs/common/config/init.js +3 -0
  5. package/dist/cjs/common/constants/env.cdn.js +1 -1
  6. package/dist/cjs/common/constants/env.npm.js +1 -1
  7. package/dist/cjs/common/harvest/harvester.js +3 -219
  8. package/dist/cjs/common/harvest/send.js +232 -0
  9. package/dist/cjs/common/harvest/types.js +9 -5
  10. package/dist/cjs/common/session/session-entity.js +8 -8
  11. package/dist/cjs/common/v2/mfe-vitals.js +303 -0
  12. package/dist/cjs/features/generic_events/aggregate/index.js +1 -1
  13. package/dist/cjs/features/generic_events/instrument/index.js +1 -1
  14. package/dist/cjs/features/logging/aggregate/index.js +5 -5
  15. package/dist/cjs/features/page_view_event/aggregate/index.js +65 -30
  16. package/dist/cjs/features/session_replay/aggregate/index.js +32 -28
  17. package/dist/cjs/features/session_trace/aggregate/index.js +29 -27
  18. package/dist/cjs/features/session_trace/constants.js +17 -3
  19. package/dist/cjs/features/utils/aggregate-base.js +2 -0
  20. package/dist/cjs/loaders/api/register.js +27 -4
  21. package/dist/esm/common/config/configurable.js +1 -1
  22. package/dist/esm/common/config/init-types.js +2 -0
  23. package/dist/esm/common/config/init.js +3 -0
  24. package/dist/esm/common/constants/env.cdn.js +1 -1
  25. package/dist/esm/common/constants/env.npm.js +1 -1
  26. package/dist/esm/common/harvest/harvester.js +3 -219
  27. package/dist/esm/common/harvest/send.js +226 -0
  28. package/dist/esm/common/harvest/types.js +9 -5
  29. package/dist/esm/common/session/session-entity.js +9 -9
  30. package/dist/esm/common/v2/mfe-vitals.js +298 -0
  31. package/dist/esm/features/generic_events/aggregate/index.js +1 -1
  32. package/dist/esm/features/generic_events/instrument/index.js +1 -1
  33. package/dist/esm/features/logging/aggregate/index.js +5 -5
  34. package/dist/esm/features/page_view_event/aggregate/index.js +64 -29
  35. package/dist/esm/features/session_replay/aggregate/index.js +32 -28
  36. package/dist/esm/features/session_trace/aggregate/index.js +30 -28
  37. package/dist/esm/features/session_trace/constants.js +16 -2
  38. package/dist/esm/features/utils/aggregate-base.js +2 -0
  39. package/dist/esm/loaders/api/register.js +27 -4
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/dist/types/common/config/init-types.d.ts +6 -0
  42. package/dist/types/common/config/init.d.ts.map +1 -1
  43. package/dist/types/common/harvest/harvester.d.ts +0 -17
  44. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  45. package/dist/types/common/harvest/send.d.ts +9 -0
  46. package/dist/types/common/harvest/send.d.ts.map +1 -0
  47. package/dist/types/common/harvest/types.d.ts +39 -10
  48. package/dist/types/common/harvest/types.d.ts.map +1 -1
  49. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  50. package/dist/types/common/v2/mfe-vitals.d.ts +14 -0
  51. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -0
  52. package/dist/types/features/page_view_event/aggregate/index.d.ts +1 -0
  53. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  54. package/dist/types/features/session_replay/aggregate/index.d.ts +2 -2
  55. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  56. package/dist/types/features/session_trace/aggregate/index.d.ts +1 -1
  57. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  58. package/dist/types/features/session_trace/constants.d.ts +17 -0
  59. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  60. package/dist/types/loaders/api/register.d.ts.map +1 -1
  61. package/package.json +2 -2
  62. package/src/common/config/configurable.js +1 -1
  63. package/src/common/config/init-types.js +2 -0
  64. package/src/common/config/init.js +2 -1
  65. package/src/common/harvest/harvester.js +3 -202
  66. package/src/common/harvest/send.js +209 -0
  67. package/src/common/harvest/types.js +9 -5
  68. package/src/common/session/session-entity.js +8 -9
  69. package/src/common/v2/mfe-vitals.js +285 -0
  70. package/src/features/generic_events/aggregate/index.js +1 -1
  71. package/src/features/generic_events/instrument/index.js +1 -1
  72. package/src/features/logging/aggregate/index.js +5 -5
  73. package/src/features/page_view_event/aggregate/index.js +52 -27
  74. package/src/features/session_replay/aggregate/index.js +30 -27
  75. package/src/features/session_trace/aggregate/index.js +31 -28
  76. package/src/features/session_trace/constants.js +7 -1
  77. package/src/features/utils/aggregate-base.js +2 -0
  78. package/src/loaders/api/register.js +25 -4
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { JSERRORS, RUM, EVENTS, FEATURE_NAMES, BLOBS, LOGS } from '../../loaders/features/features';
6
+ import { VERSION } from "../constants/env.npm";
7
+ import { globalScope, isWorkerScope } from '../constants/runtime';
8
+ import { handle } from '../event-emitter/handle';
9
+ import { eventListenerOpts } from '../event-listener/event-listener-opts';
10
+ import { now } from '../timing/now';
11
+ import { cleanURL } from '../url/clean-url';
12
+ import { obj, param } from '../url/encode';
13
+ import { warn } from '../util/console';
14
+ import { stringify } from '../util/stringify';
15
+ import { xhr as xhrMethod, xhrFetch as fetchMethod } from '../util/submit-data';
16
+ import { dispatchGlobalEvent } from '../dispatch/global-event';
17
+
18
+ /**
19
+ * @typedef {import('./types.js').NetworkSendSpec} NetworkSendSpec
20
+ */
21
+
22
+ const warnings = {};
23
+
24
+ /**
25
+ * Initiate a harvest call.
26
+ * @param {object} agentRef The initialized agent reference
27
+ * @param {NetworkSendSpec} spec Specification for sending data
28
+ * @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
29
+ */
30
+ export function send(agentRef, {
31
+ endpoint,
32
+ payload,
33
+ localOpts = {},
34
+ submitMethod,
35
+ cbFinished,
36
+ raw,
37
+ featureName,
38
+ endpointVersion = 1,
39
+ harvesterObfuscator
40
+ }) {
41
+ if (!agentRef.info.errorBeacon) return false;
42
+ let {
43
+ body,
44
+ qs
45
+ } = cleanPayload(payload);
46
+ if (Object.keys(body).length === 0 && !localOpts.sendEmptyBody) {
47
+ // if there's no body to send, just run onfinish stuff and return
48
+ if (cbFinished) cbFinished({
49
+ sent: false
50
+ });
51
+ return false;
52
+ }
53
+ const protocol = agentRef.init.ssl === false ? 'http' : 'https';
54
+ const perceivedBeacon = agentRef.init.proxy.beacon || agentRef.info.errorBeacon;
55
+ const url = raw ? "".concat(protocol, "://").concat(perceivedBeacon, "/").concat(endpoint) : "".concat(protocol, "://").concat(perceivedBeacon).concat(endpoint !== RUM ? '/' + endpoint : '', "/").concat(endpointVersion, "/").concat(agentRef.info.licenseKey);
56
+ const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) : '';
57
+ let payloadParams = obj(qs, agentRef.runtime.maxBytes);
58
+ if (baseParams === '' && payloadParams.startsWith('&')) {
59
+ payloadParams = payloadParams.substring(1);
60
+ }
61
+ const fullUrl = "".concat(url, "?").concat(baseParams).concat(payloadParams);
62
+ const gzip = !!qs?.attributes?.includes('gzip');
63
+
64
+ // all gzipped data is already in the correct format and needs no transformation
65
+ // all features going to 'events' endpoint should already be serialized & stringified
66
+ let stringBody = gzip || endpoint === EVENTS ? body : stringify(body);
67
+
68
+ // If body is null, undefined, or an empty object or array after stringifying, send an empty string instead.
69
+ if (!stringBody || stringBody.length === 0 || stringBody === '{}' || stringBody === '[]') stringBody = '';
70
+
71
+ // Warn--once per endpoint--if the agent tries to send large payloads
72
+ if (endpoint !== BLOBS && stringBody.length > 750000 && (warnings[endpoint] = (warnings[endpoint] || 0) + 1) === 1) warn(28, endpoint);
73
+ const headers = [{
74
+ key: 'content-type',
75
+ value: 'text/plain'
76
+ }];
77
+
78
+ /* Since workers don't support sendBeacon right now, they can only use XHR method.
79
+ Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
80
+ we just make a BLOCKING request--trivial impact--with the remaining data as a temp fill-in for sendBeacon.
81
+ Following the removal of img-element method. */
82
+ let result = submitMethod({
83
+ url: fullUrl,
84
+ body: stringBody,
85
+ sync: localOpts.isFinalHarvest && isWorkerScope,
86
+ headers
87
+ });
88
+ if (!localOpts.isFinalHarvest && cbFinished) {
89
+ // final harvests don't hold onto buffer data (shouldRetryOnFail is false), so cleanup isn't needed
90
+ if (submitMethod === xhrMethod) {
91
+ result.addEventListener('loadend', function () {
92
+ // `this` here in block refers to the XHR object in this scope, do not change the anon function to an arrow function
93
+ // status 0 refers to a local error, such as CORS or network failure, or a blocked request by the browser (e.g. adblocker)
94
+ const cbResult = {
95
+ sent: this.status !== 0,
96
+ status: this.status,
97
+ retry: shouldRetry(this.status),
98
+ fullUrl,
99
+ xhr: this,
100
+ responseText: this.responseText
101
+ };
102
+ cbFinished(cbResult);
103
+
104
+ /** temporary audit of consistency of harvest metadata flags */
105
+ if (!shouldRetry(this.status)) trackHarvestMetadata();
106
+ }, eventListenerOpts(false));
107
+ } else if (submitMethod === fetchMethod) {
108
+ result.then(async function (response) {
109
+ const status = response.status;
110
+ const cbResult = {
111
+ sent: true,
112
+ status,
113
+ retry: shouldRetry(status),
114
+ fullUrl,
115
+ fetchResponse: response,
116
+ responseText: await response.text()
117
+ };
118
+ cbFinished(cbResult);
119
+ /** temporary audit of consistency of harvest metadata flags */
120
+ if (!shouldRetry(status)) trackHarvestMetadata();
121
+ });
122
+ }
123
+ function trackHarvestMetadata() {
124
+ try {
125
+ if (featureName === FEATURE_NAMES.jserrors && !body?.err) return;
126
+ const hasReplay = baseParams.includes('hr=1');
127
+ const hasTrace = baseParams.includes('ht=1');
128
+ const hasError = qs?.attributes?.includes('hasError=true');
129
+ handle('harvest-metadata', [{
130
+ [featureName]: {
131
+ ...(hasReplay && {
132
+ hasReplay
133
+ }),
134
+ ...(hasTrace && {
135
+ hasTrace
136
+ }),
137
+ ...(hasError && {
138
+ hasError
139
+ })
140
+ }
141
+ }], undefined, FEATURE_NAMES.metrics, agentRef.ee);
142
+ } catch (err) {
143
+ // do nothing
144
+ }
145
+ }
146
+ }
147
+ dispatchGlobalEvent({
148
+ drained: !!agentRef.runtime?.activatedFeatures,
149
+ type: 'data',
150
+ name: 'harvest',
151
+ feature: featureName,
152
+ data: {
153
+ endpoint,
154
+ headers,
155
+ payload,
156
+ submitMethod: getSubmitMethodName(),
157
+ raw,
158
+ synchronousXhr: !!(localOpts.isFinalHarvest && isWorkerScope)
159
+ }
160
+ });
161
+ return true;
162
+ function shouldRetry(status) {
163
+ switch (status) {
164
+ case 408:
165
+ case 429:
166
+ case 500:
167
+ return true;
168
+ }
169
+ return status >= 502 && status <= 504 || status >= 512 && status <= 530;
170
+ }
171
+ function getSubmitMethodName() {
172
+ if (submitMethod === xhrMethod) return 'xhr';
173
+ if (submitMethod === fetchMethod) return 'fetch';
174
+ return 'beacon';
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Cleans and returns a payload object containing a body and qs
180
+ * object with key/value pairs. KV pairs where the value is null,
181
+ * undefined, or an empty string are removed to save on transmission
182
+ * size.
183
+ * @param {HarvestPayload} payload Payload to be sent to the endpoint.
184
+ * @returns {HarvestPayload} Cleaned payload payload to be sent to the endpoint.
185
+ */
186
+ function cleanPayload(payload = {}) {
187
+ const clean = input => {
188
+ if (typeof Uint8Array !== 'undefined' && input instanceof Uint8Array || Array.isArray(input)) return input;
189
+ if (typeof input === 'string') return input;
190
+ return Object.entries(input || {}).reduce((accumulator, [key, value]) => {
191
+ if (typeof value === 'number' || typeof value === 'string' && value.length > 0 || typeof value === 'object' && Object.keys(value || {}).length > 0) {
192
+ accumulator[key] = value;
193
+ }
194
+ return accumulator;
195
+ }, {});
196
+ };
197
+ return {
198
+ body: clean(payload.body),
199
+ qs: clean(payload.qs)
200
+ };
201
+ }
202
+
203
+ // The stuff that gets sent every time.
204
+ function baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) {
205
+ const cleanedURL = cleanURL('' + globalScope.location);
206
+ const ref = harvesterObfuscator?.obfuscateString(cleanedURL) ?? cleanedURL;
207
+ const session = agentRef.runtime.session;
208
+ const hr = !!session?.state.sessionReplaySentFirstChunk && session?.state.sessionReplayMode === 1 && endpoint !== JSERRORS;
209
+ const ht = !!session?.state.traceHarvestStarted && session?.state.sessionTraceMode === 1 && ![LOGS, BLOBS].includes(endpoint);
210
+ const qps = ['a=' + agentRef.info.applicationID, param('sa', agentRef.info.sa ? '' + agentRef.info.sa : ''), param('v', VERSION), transactionNameParam(), param('ct', agentRef.runtime.customTransaction), '&rst=' + now(), '&ck=0',
211
+ // ck param DEPRECATED - still expected by backend
212
+ '&s=' + (session?.state.value || '0'),
213
+ // the 0 id encaps all untrackable and default traffic
214
+ param('ref', ref), param('ptid', agentRef.runtime.ptid ? '' + agentRef.runtime.ptid : '')];
215
+ if (hr) qps.push(param('hr', '1', qs));
216
+ if (ht) qps.push(param('ht', '1', qs));
217
+ return qps.join('');
218
+
219
+ // Constructs the transaction name param for the beacon URL.
220
+ // Prefers the obfuscated transaction name over the plain text.
221
+ // Falls back to making up a name.
222
+ function transactionNameParam() {
223
+ if (agentRef.info.transactionName) return param('to', agentRef.info.transactionName);
224
+ return param('t', agentRef.info.tNamePlain || 'Unnamed Transaction');
225
+ }
226
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
@@ -23,11 +23,15 @@
23
23
  * @typedef {object} NetworkSendSpec
24
24
  * @property {HarvestEndpointIdentifier} endpoint The endpoint to use (jserrors, events, resources etc.)
25
25
  * @property {HarvestPayload} payload Object representing payload.
26
- * @property {object} localOpts Additional options for sending data
27
- * @property {boolean} localOpts.isFinalHarvest Specify whether the call is a final harvest during page unload.
28
- * @property {boolean} localOpts.sendEmptyBody Specify whether the call should be made even if the body is empty. Useful for rum calls.
29
- * @property {boolean} localOpts.forceNoRetry Don't save the buffered data in the case of a need to retry the transmission.
26
+ * @property {object} [localOpts] Additional options for sending data
27
+ * @property {boolean} [localOpts.isFinalHarvest] Specify whether the call is a final harvest during page unload.
28
+ * @property {boolean} [localOpts.sendEmptyBody] Specify whether the call should be made even if the body is empty. Useful for rum calls.
29
+ * @property {boolean} [localOpts.forceNoRetry] Don't save the buffered data in the case of a need to retry the transmission.
30
30
  * @property {import('../util/submit-data.js').NetworkMethods} submitMethod The network method to use {@link ../util/submit-data.js}
31
+ * @property {function(object): void} [cbFinished] Callback invoked with the result once the network request completes. Receives an object with sent, status, retry, fullUrl, responseText, and either xhr or fetchResponse.
32
+ * @property {boolean} [raw] If true, skips standard versioned URL path construction and uses a bare endpoint URL.
33
+ * @property {string} [featureName] The feature name associated with this harvest, used for metadata tracking and the global harvest event.
34
+ * @property {number} [endpointVersion] The endpoint version included in the URL path. Defaults to 1.
31
35
  */
32
36
 
33
37
  export const unused = {};
@@ -7,7 +7,7 @@ import { warn } from '../util/console';
7
7
  import { stringify } from '../util/stringify';
8
8
  import { Timer } from '../timer/timer';
9
9
  import { isBrowserScope } from '../constants/runtime';
10
- import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS, MODE, SESSION_EVENTS, SESSION_EVENT_TYPES, SESSION_STORAGE_KEY_PREFIX } from './constants';
10
+ import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS, SESSION_EVENTS, SESSION_EVENT_TYPES, SESSION_STORAGE_KEY_PREFIX } from './constants';
11
11
  import { InteractionTimer } from '../timer/interaction-timer';
12
12
  import { wrapEvents } from '../wrap/wrap-events';
13
13
  import { getModeledObject } from '../config/configurable';
@@ -15,26 +15,27 @@ import { handle } from '../event-emitter/handle';
15
15
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants';
16
16
  import { FEATURE_NAMES } from '../../loaders/features/features';
17
17
  import { windowAddEventListener } from '../event-listener/event-listener-opts';
18
- import { LOGGING_MODE } from '../../features/logging/constants';
19
18
 
20
- // this is what can be stored in local storage (not enforced but probably should be)
19
+ // this is what can be stored in local storage (enforced during reads)
21
20
  // these values should sync between local storage and the parent class props
22
21
  const model = {
23
22
  value: '',
24
23
  inactiveAt: 0,
25
24
  expiresAt: 0,
26
25
  updatedAt: Date.now(),
27
- sessionReplayMode: MODE.OFF,
26
+ sessionReplayMode: null,
28
27
  sessionReplaySentFirstChunk: false,
29
- sessionTraceMode: MODE.OFF,
28
+ sessionTraceMode: null,
30
29
  traceHarvestStarted: false,
31
- loggingMode: LOGGING_MODE.OFF,
32
- logApiMode: LOGGING_MODE.OFF,
30
+ loggingMode: null,
31
+ logApiMode: null,
33
32
  serverTimeDiff: null,
34
33
  // set by TimeKeeper; "undefined" value will not be stringified and stored but "null" will
35
34
  custom: {},
36
35
  numOfResets: 0,
37
- consent: false // set by consent() API call
36
+ consent: false,
37
+ // set by consent() API call
38
+ cachedRumResponse: null
38
39
  };
39
40
  export class SessionEntity {
40
41
  /**
@@ -246,7 +247,6 @@ export class SessionEntity {
246
247
  this.expiresTimer?.clear?.();
247
248
  delete this.isNew;
248
249
  this.setup({
249
- agentRef: this.agentRef,
250
250
  key: this.key,
251
251
  storage: this.storage,
252
252
  expiresMs: this.expiresMs,
@@ -0,0 +1,298 @@
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
+
9
+ /**
10
+ * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
11
+ */
12
+
13
+ const isObservable = node => {
14
+ try {
15
+ return node?.textContent?.trim() || ['img', 'video', 'canvas', 'svg'].includes(node?.nodeName?.toLowerCase());
16
+ } catch (e) {
17
+ return false;
18
+ }
19
+ };
20
+
21
+ /**
22
+ * Check if node is within a specific MFE
23
+ * @param {Node} node - DOM node to check
24
+ * @param {string} id - MFE ID to match
25
+ * @returns {boolean}
26
+ */
27
+ const isInMFE = (node, id) => {
28
+ if (!node || !id) return false;
29
+ try {
30
+ let curr = node.nodeType === 1 ? node : node.parentElement;
31
+ while (curr?.tagName) {
32
+ if (curr.dataset?.nrMfeId === id) return true;
33
+ curr = curr.parentNode;
34
+ }
35
+ } catch (e) {}
36
+ return false;
37
+ };
38
+
39
+ /**
40
+ * Create mutation observer for MFE nodes
41
+ * @param {string} id - MFE ID to track
42
+ * @param {Function} onMatch - Callback when matching node is *added*
43
+ * @returns {MutationObserver}
44
+ */
45
+ const observeMutations = (id, onMatch) => {
46
+ // Try to find existing MFE root
47
+ const mfeRoot = globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"));
48
+ let observingRoot = !!mfeRoot;
49
+ const obs = new globalScope.MutationObserver(mutations => {
50
+ mutations.forEach(m => {
51
+ m.addedNodes.forEach(node => {
52
+ // Check if this is the MFE root being added
53
+ const elem = node.nodeType === 1 ? node : null;
54
+ if (elem?.dataset?.nrMfeId === id) {
55
+ // Found the root! Lets switch to observing just this subtree for performance reasons
56
+ obs.disconnect();
57
+ obs.observe(elem, {
58
+ childList: true,
59
+ subtree: true
60
+ });
61
+ observingRoot = true;
62
+ }
63
+
64
+ // Only check isInMFE if we're observing the whole document; skip expensive ancestor walk when observing root
65
+ if (isObservable(node) && (observingRoot || isInMFE(node, id))) {
66
+ onMatch(node, obs);
67
+ }
68
+ });
69
+ });
70
+ });
71
+
72
+ // If root exists, observe just that subtree; otherwise observe document to catch when root is added
73
+ obs.observe(mfeRoot || globalScope.document, {
74
+ childList: true,
75
+ subtree: true
76
+ });
77
+ return obs;
78
+ };
79
+
80
+ /**
81
+ * Create performance observer for MFE entries
82
+ * @param {Array} observers - Array to track observers
83
+ * @param {Object} config - Observer configuration
84
+ * @param {Function} onEntry - Callback for each entry
85
+ * @returns {PerformanceObserver|null} Observer if successful, null if unsupported
86
+ */
87
+ const observePerformance = (observers, config, onEntry) => {
88
+ try {
89
+ const obs = new globalScope.PerformanceObserver(list => {
90
+ list.getEntries().forEach(onEntry);
91
+ });
92
+ obs.observe(config);
93
+ observers.push(obs);
94
+ return obs;
95
+ } catch (e) {
96
+ return null;
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Tracks all Core Web Vitals for a specific MFE.
102
+ * @param {string} id - The MFE ID to track
103
+ * @returns {{fcp: object|null, lcp: object|null, cls: object|null, inp: object|null, disconnect: Function}}
104
+ */
105
+ export function trackMFEVitals(id, timings) {
106
+ let fcpObservedAt = null;
107
+ let lcpObservedAt = null;
108
+ const getTimeRelativeToScriptStart = capturedAt => {
109
+ if (capturedAt === null) return null;
110
+ return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
111
+ };
112
+ const vitals = {
113
+ fcp: {
114
+ get value() {
115
+ return getTimeRelativeToScriptStart(fcpObservedAt);
116
+ }
117
+ },
118
+ lcp: {
119
+ get value() {
120
+ return getTimeRelativeToScriptStart(lcpObservedAt);
121
+ }
122
+ },
123
+ cls: {
124
+ value: null
125
+ },
126
+ inp: {
127
+ value: null
128
+ },
129
+ disconnect: () => {}
130
+ };
131
+ if (!id || !isBrowserScope || !globalScope.MutationObserver || !globalScope.PerformanceObserver) return vitals;
132
+ const observers = [];
133
+ const populateVitalMinimums = () => {
134
+ fcpObservedAt ??= now();
135
+ lcpObservedAt ??= now();
136
+ vitals.cls.value ??= 0;
137
+ };
138
+
139
+ // if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
140
+ if (globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"))) populateVitalMinimums();
141
+
142
+ // Track FCP - first contentful paint
143
+ observeMutations(id, (_, obs) => {
144
+ // An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
145
+ populateVitalMinimums();
146
+ obs.disconnect();
147
+ });
148
+
149
+ // Track LCP - largest contentful paint
150
+ let largestSize = 0;
151
+ let resizeObs = null;
152
+ const observedElements = new Set();
153
+ try {
154
+ resizeObs = new globalScope.ResizeObserver(entries => {
155
+ entries.forEach(entry => {
156
+ try {
157
+ const size = entry.contentRect.width * entry.contentRect.height;
158
+ if (size > largestSize) {
159
+ largestSize = size;
160
+ lcpObservedAt = now();
161
+ }
162
+ resizeObs.unobserve(entry.target);
163
+ } catch (e) {
164
+ // Element may be detached from DOM
165
+ }
166
+ });
167
+ });
168
+ } catch (e) {
169
+ // ResizeObserver not supported
170
+ }
171
+ const lcpObs = observeMutations(id, node => {
172
+ // an observed "LCP" means _something_ rendered, so at minimum we can make sure all the baseline values are populated for the vitals
173
+ populateVitalMinimums();
174
+ if (resizeObs) {
175
+ try {
176
+ const elem = node.nodeType === 1 ? node : node.parentElement;
177
+ if (elem && !observedElements.has(elem)) {
178
+ observedElements.add(elem);
179
+
180
+ // For media elements, wait for content to load before observing size
181
+ if (elem.tagName === 'IMG') {
182
+ if (elem.complete) {
183
+ resizeObs.observe(elem);
184
+ } else {
185
+ elem.addEventListener('load', () => {
186
+ resizeObs.observe(elem);
187
+ }, {
188
+ once: true
189
+ });
190
+ }
191
+ } else if (elem.tagName === 'VIDEO') {
192
+ // For video, wait for first frame (HAVE_CURRENT_DATA = 2)
193
+ if (elem.readyState >= 2) {
194
+ resizeObs.observe(elem);
195
+ } else {
196
+ elem.addEventListener('loadeddata', () => {
197
+ resizeObs.observe(elem);
198
+ }, {
199
+ once: true
200
+ });
201
+ }
202
+ } else {
203
+ // For other elements, observe immediately
204
+ resizeObs.observe(elem);
205
+ }
206
+ }
207
+ } catch (e) {
208
+ // Element may not be observable
209
+ }
210
+ }
211
+ });
212
+ if (resizeObs) observers.push(resizeObs);
213
+ observers.push(lcpObs);
214
+
215
+ // Track CLS - cumulative layout shift
216
+ // Initialize CLS to 0 if browser supports it
217
+ observePerformance(observers, {
218
+ type: 'layout-shift',
219
+ buffered: true
220
+ }, entry => {
221
+ if (entry.hadRecentInput) return;
222
+ (entry.sources || []).some(source => {
223
+ if (isInMFE(source.node, id)) {
224
+ // 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
225
+ populateVitalMinimums();
226
+ vitals.cls.value += entry.value;
227
+ return true;
228
+ }
229
+ return false;
230
+ });
231
+ });
232
+
233
+ // Track INP - interaction to next paint
234
+ observePerformance(observers, {
235
+ type: 'event',
236
+ buffered: true,
237
+ durationThreshold: 40
238
+ }, entry => {
239
+ if (!entry.interactionId || !isInMFE(entry.target, id)) return;
240
+ if (vitals.inp.value === null || entry.duration > vitals.inp.value) {
241
+ // 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
242
+ populateVitalMinimums();
243
+ vitals.inp.value = entry.duration;
244
+ }
245
+ });
246
+ const interactionEvents = ['pointerdown', 'keydown'];
247
+ const disconnectInteractionListeners = () => {
248
+ interactionEvents.forEach(type => {
249
+ globalScope.removeEventListener(type, handleInteraction, {
250
+ passive: true
251
+ });
252
+ });
253
+ };
254
+
255
+ // Disconnect all observers
256
+ vitals.disconnect = () => {
257
+ // Disconnect all observers
258
+ observers.forEach(obs => {
259
+ try {
260
+ obs?.disconnect();
261
+ } catch (e) {
262
+ // Observer may already be disconnected
263
+ }
264
+ });
265
+ disconnectInteractionListeners();
266
+ };
267
+
268
+ // Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
269
+ // CLS and INP continue tracking until visibility change or deregister
270
+ const disconnectLCP = () => {
271
+ try {
272
+ lcpObs?.disconnect();
273
+ resizeObs?.disconnect();
274
+ } catch (e) {
275
+ // Observer may already be disconnected
276
+ }
277
+ };
278
+ const handleInteraction = event => {
279
+ if (!isInMFE(event?.target, id)) return;
280
+ disconnectLCP();
281
+ disconnectInteractionListeners();
282
+ };
283
+ interactionEvents.forEach(type => {
284
+ globalScope.addEventListener(type, handleInteraction, {
285
+ passive: true
286
+ });
287
+ })
288
+
289
+ // Disconnect all observers on visibility change or page unload
290
+ ;
291
+ ['visibilitychange', 'pagehide'].forEach(type => {
292
+ globalScope.addEventListener(type, vitals.disconnect, {
293
+ once: true,
294
+ passive: true
295
+ });
296
+ });
297
+ return vitals;
298
+ }
@@ -254,7 +254,7 @@ export class Aggregate extends AggregateBase {
254
254
  };
255
255
  this.addEvent(event, target);
256
256
  }, this.featureName, this.ee);
257
- if (agentRef.init.feature_flags.includes('websockets')) {
257
+ if (agentRef.init.feature_flags.includes('websockets') || agentRef.init.web_sockets?.enabled) {
258
258
  registerHandler('ws-complete', nrData => {
259
259
  const event = {
260
260
  ...nrData,
@@ -25,7 +25,7 @@ export class Instrument extends InstrumentBase {
25
25
  static featureName = FEATURE_NAME;
26
26
  constructor(agentRef) {
27
27
  super(agentRef, FEATURE_NAME);
28
- const websocketsEnabled = agentRef.init.feature_flags.includes('websockets');
28
+ const websocketsEnabled = agentRef.init.feature_flags.includes('websockets') || agentRef.init.web_sockets?.enabled;
29
29
  const securityPolicyViolationEnabled = !agentRef.init.feature_flags.includes('no_spv');
30
30
 
31
31
  /** config values that gate whether the generic events aggregator should be imported at all */
@@ -54,7 +54,7 @@ export class Aggregate extends AggregateBase {
54
54
  }; // likewise, don't want to overwrite the mode if it was set already
55
55
  const session = this.agentRef.runtime.session;
56
56
  if (canEnableSessionTracking(agentRef.init) && session) {
57
- if (session.isNew) this.#syncWithSessionManager();else updateLocalLoggingMode(session.state.loggingMode, session.state.logApiMode);
57
+ if (session.state.loggingMode === null || session.state.logApiMode === null) this.#writeToStorage(this.loggingMode);else updateLocalLoggingMode(session.state.loggingMode, session.state.logApiMode);
58
58
  }
59
59
  if (this.loggingMode.auto === LOGGING_MODE.OFF && this.loggingMode.api === LOGGING_MODE.OFF) {
60
60
  this.blocked = true;
@@ -162,13 +162,13 @@ export class Aggregate extends AggregateBase {
162
162
  auto: LOGGING_MODE.OFF,
163
163
  api: LOGGING_MODE.OFF
164
164
  };
165
- this.#syncWithSessionManager();
165
+ this.#writeToStorage(this.loggingMode);
166
166
  this.deregisterDrain();
167
167
  }
168
- #syncWithSessionManager() {
168
+ #writeToStorage(logModes) {
169
169
  this.agentRef.runtime.session?.write({
170
- loggingMode: this.loggingMode.auto,
171
- logApiMode: this.loggingMode.api
170
+ loggingMode: logModes.auto,
171
+ logApiMode: logModes.api
172
172
  });
173
173
  }
174
174
  }