@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.
- package/CHANGELOG.md +9 -0
- package/dist/cjs/common/config/configurable.js +1 -1
- package/dist/cjs/common/config/init-types.js +2 -0
- package/dist/cjs/common/config/init.js +3 -0
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/harvest/harvester.js +3 -219
- package/dist/cjs/common/harvest/send.js +232 -0
- package/dist/cjs/common/harvest/types.js +9 -5
- package/dist/cjs/common/session/session-entity.js +8 -8
- package/dist/cjs/common/v2/mfe-vitals.js +303 -0
- package/dist/cjs/features/generic_events/aggregate/index.js +1 -1
- package/dist/cjs/features/generic_events/instrument/index.js +1 -1
- package/dist/cjs/features/logging/aggregate/index.js +5 -5
- package/dist/cjs/features/page_view_event/aggregate/index.js +65 -30
- package/dist/cjs/features/session_replay/aggregate/index.js +32 -28
- package/dist/cjs/features/session_trace/aggregate/index.js +29 -27
- package/dist/cjs/features/session_trace/constants.js +17 -3
- package/dist/cjs/features/utils/aggregate-base.js +2 -0
- package/dist/cjs/loaders/api/register.js +27 -4
- package/dist/esm/common/config/configurable.js +1 -1
- package/dist/esm/common/config/init-types.js +2 -0
- package/dist/esm/common/config/init.js +3 -0
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/harvest/harvester.js +3 -219
- package/dist/esm/common/harvest/send.js +226 -0
- package/dist/esm/common/harvest/types.js +9 -5
- package/dist/esm/common/session/session-entity.js +9 -9
- package/dist/esm/common/v2/mfe-vitals.js +298 -0
- package/dist/esm/features/generic_events/aggregate/index.js +1 -1
- package/dist/esm/features/generic_events/instrument/index.js +1 -1
- package/dist/esm/features/logging/aggregate/index.js +5 -5
- package/dist/esm/features/page_view_event/aggregate/index.js +64 -29
- package/dist/esm/features/session_replay/aggregate/index.js +32 -28
- package/dist/esm/features/session_trace/aggregate/index.js +30 -28
- package/dist/esm/features/session_trace/constants.js +16 -2
- package/dist/esm/features/utils/aggregate-base.js +2 -0
- package/dist/esm/loaders/api/register.js +27 -4
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/config/init-types.d.ts +6 -0
- package/dist/types/common/config/init.d.ts.map +1 -1
- package/dist/types/common/harvest/harvester.d.ts +0 -17
- 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/session/session-entity.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/features/page_view_event/aggregate/index.d.ts +1 -0
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +2 -2
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +1 -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/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/loaders/api/register.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/config/configurable.js +1 -1
- package/src/common/config/init-types.js +2 -0
- package/src/common/config/init.js +2 -1
- package/src/common/harvest/harvester.js +3 -202
- package/src/common/harvest/send.js +209 -0
- package/src/common/harvest/types.js +9 -5
- package/src/common/session/session-entity.js +8 -9
- package/src/common/v2/mfe-vitals.js +285 -0
- package/src/features/generic_events/aggregate/index.js +1 -1
- package/src/features/generic_events/instrument/index.js +1 -1
- package/src/features/logging/aggregate/index.js +5 -5
- package/src/features/page_view_event/aggregate/index.js +52 -27
- package/src/features/session_replay/aggregate/index.js +30 -27
- package/src/features/session_trace/aggregate/index.js +31 -28
- package/src/features/session_trace/constants.js +7 -1
- package/src/features/utils/aggregate-base.js +2 -0
- 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-
|
|
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,
|
|
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 (
|
|
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:
|
|
26
|
+
sessionReplayMode: null,
|
|
28
27
|
sessionReplaySentFirstChunk: false,
|
|
29
|
-
sessionTraceMode:
|
|
28
|
+
sessionTraceMode: null,
|
|
30
29
|
traceHarvestStarted: false,
|
|
31
|
-
loggingMode:
|
|
32
|
-
logApiMode:
|
|
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
|
|
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.
|
|
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.#
|
|
165
|
+
this.#writeToStorage(this.loggingMode);
|
|
166
166
|
this.deregisterDrain();
|
|
167
167
|
}
|
|
168
|
-
#
|
|
168
|
+
#writeToStorage(logModes) {
|
|
169
169
|
this.agentRef.runtime.session?.write({
|
|
170
|
-
loggingMode:
|
|
171
|
-
logApiMode:
|
|
170
|
+
loggingMode: logModes.auto,
|
|
171
|
+
logApiMode: logModes.api
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
174
|
}
|