@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,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.canCapturePayload = canCapturePayload;
|
|
7
|
+
exports.createStringAdders = createStringAdders;
|
|
8
|
+
exports.isLikelyHumanReadable = isLikelyHumanReadable;
|
|
9
|
+
exports.parseQueryString = parseQueryString;
|
|
10
|
+
exports.truncateAsString = truncateAsString;
|
|
11
|
+
var _stringify = require("../util/stringify");
|
|
12
|
+
var _constants = require("../../features/ajax/constants");
|
|
13
|
+
/**
|
|
14
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
15
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Determines whether payload data should be captured based on the capture mode setting,
|
|
20
|
+
* HTTP status code, and GraphQL error status.
|
|
21
|
+
* @param {string} captureMode - The capture mode setting ('none', 'all', or 'failures')
|
|
22
|
+
* @param {number} statusCode - The HTTP status code
|
|
23
|
+
* @param {boolean} hasGQLErrors - Whether the response contains GraphQL errors
|
|
24
|
+
* @returns {boolean} True if payload should be captured
|
|
25
|
+
*/
|
|
26
|
+
function canCapturePayload(captureMode, statusCode, hasGQLErrors) {
|
|
27
|
+
if (captureMode === _constants.CAPTURE_PAYLOAD_SETTINGS.ALL) return true;
|
|
28
|
+
if (!captureMode || captureMode === _constants.CAPTURE_PAYLOAD_SETTINGS.NONE) return false;
|
|
29
|
+
|
|
30
|
+
// Default "failures" mode
|
|
31
|
+
return statusCode === 0 || statusCode >= 400 || hasGQLErrors === true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses a query string into an object of key-value pairs.
|
|
36
|
+
* @param {string} search a query string starting with "?" or without (ex. new URL(...).search)
|
|
37
|
+
* @returns {Object|undefined} Parsed query parameters as key-value pairs. Returns undefined if no valid parameters are found.
|
|
38
|
+
*/
|
|
39
|
+
function parseQueryString(search) {
|
|
40
|
+
if (!search || search.length === 0) return;
|
|
41
|
+
const queryParams = {};
|
|
42
|
+
try {
|
|
43
|
+
const searchParams = new URLSearchParams(search);
|
|
44
|
+
searchParams.forEach(function (value, key) {
|
|
45
|
+
queryParams[key] = value;
|
|
46
|
+
});
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// Fallback for environments without URLSearchParams
|
|
49
|
+
}
|
|
50
|
+
return queryParams;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Determines if the given content type is likely to be human-readable (text-based).
|
|
55
|
+
* @param {Object} headers - The headers object containing content-type
|
|
56
|
+
* @param {*} data - The data to check
|
|
57
|
+
* @returns {boolean} True if the content type is human-readable (text-based)
|
|
58
|
+
*/
|
|
59
|
+
function isLikelyHumanReadable(headers, data) {
|
|
60
|
+
if (!headers) return typeof data === 'string';
|
|
61
|
+
var contentType = headers['content-type'];
|
|
62
|
+
if (!contentType) return typeof data === 'string';
|
|
63
|
+
// Normalize to lowercase and extract the mime type (ignore charset, etc.)
|
|
64
|
+
var mimeType = contentType.toLowerCase().split(';')[0].trim();
|
|
65
|
+
|
|
66
|
+
// Check for text/* types
|
|
67
|
+
if (mimeType.indexOf('text/') === 0) return true;
|
|
68
|
+
|
|
69
|
+
// Check for specific application/* types
|
|
70
|
+
var readableAppTypes = ['/json', '/xml', '/xhtml+xml', '/ld+json', '/yaml', '/x-www-form-urlencoded'];
|
|
71
|
+
for (var i = 0; i < readableAppTypes.length; i++) {
|
|
72
|
+
if (mimeType === 'application' + readableAppTypes[i]) return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Truncates a string to ensure its UTF-8 byte length does not exceed 4092 bytes. If truncation is necessary,
|
|
79
|
+
* the string is cut off at a character boundary to avoid breaking multi-byte characters and " ..." is appended to indicate truncation.
|
|
80
|
+
* If not a string, it is first converted to a string using JSON.stringify.
|
|
81
|
+
* @param {*} data The data to truncate.
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
function truncateAsString(data) {
|
|
85
|
+
if (!data) return data;
|
|
86
|
+
try {
|
|
87
|
+
if (typeof data !== 'string') data = (0, _stringify.stringify)(data);
|
|
88
|
+
let bytes = 0;
|
|
89
|
+
let i = 0;
|
|
90
|
+
let needsEllipsis = false;
|
|
91
|
+
while (i < data.length) {
|
|
92
|
+
const c = data.charCodeAt(i);
|
|
93
|
+
const charBytes = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0xD800 || c >= 0xE000 ? 3 : 4;
|
|
94
|
+
if (bytes + charBytes > 4092) {
|
|
95
|
+
needsEllipsis = true;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
bytes += charBytes;
|
|
99
|
+
i += charBytes === 4 ? 2 : 1;
|
|
100
|
+
}
|
|
101
|
+
return data.slice(0, i) + (needsEllipsis ? ' ...' : '');
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Creates string adder functions for BEL serialization with obfuscation and optional truncation.
|
|
109
|
+
* This ensures a single string table is used while providing separate handling for regular vs payload attributes.
|
|
110
|
+
* @param {Function} getAddStringContext - Function that creates a new string table context
|
|
111
|
+
* @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
|
|
112
|
+
* @returns {{addString: Function, addStringWithTruncation: Function}} Object containing both string adder functions
|
|
113
|
+
*/
|
|
114
|
+
function createStringAdders(getAddStringContext, obfuscator) {
|
|
115
|
+
const addStringRaw = getAddStringContext();
|
|
116
|
+
const processString = (str, shouldTruncate) => {
|
|
117
|
+
if (typeof str === 'undefined' || str === '') return addStringRaw(str);
|
|
118
|
+
if (typeof str !== 'string') str = (0, _stringify.stringify)(str);
|
|
119
|
+
const obfuscated = obfuscator?.obfuscateString(str) ?? str;
|
|
120
|
+
const processed = shouldTruncate ? truncateAsString(obfuscated) : obfuscated;
|
|
121
|
+
return addStringRaw(processed);
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
addString: str => processString(str, false),
|
|
125
|
+
addStringWithTruncation: str => processString(str, true)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -9,7 +9,7 @@ exports.nullable = nullable;
|
|
|
9
9
|
exports.numeric = numeric;
|
|
10
10
|
var _stringify = require("../util/stringify");
|
|
11
11
|
/**
|
|
12
|
-
* Copyright 2020-
|
|
12
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
13
13
|
* SPDX-License-Identifier: Apache-2.0
|
|
14
14
|
*/
|
|
15
15
|
|
|
@@ -24,13 +24,14 @@ function numeric(n, noDefault) {
|
|
|
24
24
|
}
|
|
25
25
|
return n === undefined || n === 0 ? '' : Math.floor(n).toString(36);
|
|
26
26
|
}
|
|
27
|
-
function getAddStringContext(obfuscator) {
|
|
27
|
+
function getAddStringContext(obfuscator, truncator) {
|
|
28
28
|
let stringTableIdx = 0;
|
|
29
29
|
const stringTable = Object.prototype.hasOwnProperty.call(Object, 'create') ? Object.create(null) : {};
|
|
30
30
|
return addString;
|
|
31
31
|
function addString(str) {
|
|
32
32
|
if (typeof str === 'undefined' || str === '') return '';
|
|
33
|
-
str = obfuscator
|
|
33
|
+
str = obfuscator?.obfuscateString(String(str)) ?? String(str);
|
|
34
|
+
str = truncator?.(str) ?? str;
|
|
34
35
|
if (hasOwnProp.call(stringTable, str)) {
|
|
35
36
|
return numeric(stringTable[str], true);
|
|
36
37
|
} else {
|
|
@@ -17,30 +17,31 @@ var _handle = require("../event-emitter/handle");
|
|
|
17
17
|
var _constants2 = require("../../features/metrics/constants");
|
|
18
18
|
var _features = require("../../loaders/features/features");
|
|
19
19
|
var _eventListenerOpts = require("../event-listener/event-listener-opts");
|
|
20
|
-
var _constants3 = require("../../features/logging/constants");
|
|
21
20
|
/**
|
|
22
21
|
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
23
22
|
* SPDX-License-Identifier: Apache-2.0
|
|
24
23
|
*/
|
|
25
24
|
|
|
26
|
-
// this is what can be stored in local storage (
|
|
25
|
+
// this is what can be stored in local storage (enforced during reads)
|
|
27
26
|
// these values should sync between local storage and the parent class props
|
|
28
27
|
const model = {
|
|
29
28
|
value: '',
|
|
30
29
|
inactiveAt: 0,
|
|
31
30
|
expiresAt: 0,
|
|
32
31
|
updatedAt: Date.now(),
|
|
33
|
-
sessionReplayMode:
|
|
32
|
+
sessionReplayMode: null,
|
|
34
33
|
sessionReplaySentFirstChunk: false,
|
|
35
|
-
sessionTraceMode:
|
|
34
|
+
sessionTraceMode: null,
|
|
36
35
|
traceHarvestStarted: false,
|
|
37
|
-
loggingMode:
|
|
38
|
-
logApiMode:
|
|
36
|
+
loggingMode: null,
|
|
37
|
+
logApiMode: null,
|
|
39
38
|
serverTimeDiff: null,
|
|
40
39
|
// set by TimeKeeper; "undefined" value will not be stringified and stored but "null" will
|
|
41
40
|
custom: {},
|
|
42
41
|
numOfResets: 0,
|
|
43
|
-
consent: false
|
|
42
|
+
consent: false,
|
|
43
|
+
// set by consent() API call
|
|
44
|
+
cachedRumResponse: null
|
|
44
45
|
};
|
|
45
46
|
class SessionEntity {
|
|
46
47
|
/**
|
|
@@ -252,7 +253,6 @@ class SessionEntity {
|
|
|
252
253
|
this.expiresTimer?.clear?.();
|
|
253
254
|
delete this.isNew;
|
|
254
255
|
this.setup({
|
|
255
|
-
agentRef: this.agentRef,
|
|
256
256
|
key: this.key,
|
|
257
257
|
storage: this.storage,
|
|
258
258
|
expiresMs: this.expiresMs,
|
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.dataSize = dataSize;
|
|
7
7
|
var _stringify = require("./stringify");
|
|
8
8
|
/**
|
|
9
|
-
* Copyright 2020-
|
|
9
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
10
10
|
* SPDX-License-Identifier: Apache-2.0
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -17,7 +17,7 @@ var _stringify = require("./stringify");
|
|
|
17
17
|
* @returns {(number|undefined)} - The size of the data or undefined if size cannot be determined.
|
|
18
18
|
*/
|
|
19
19
|
function dataSize(data) {
|
|
20
|
-
if (typeof data === 'string'
|
|
20
|
+
if (typeof data === 'string') return data.length;
|
|
21
21
|
if (typeof data !== 'object') return undefined;
|
|
22
22
|
// eslint-disable-next-line
|
|
23
23
|
if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer && data.byteLength) return data.byteLength;
|
|
@@ -7,7 +7,7 @@ exports.Obfuscator = void 0;
|
|
|
7
7
|
var _protocol = require("../url/protocol");
|
|
8
8
|
var _console = require("./console");
|
|
9
9
|
/**
|
|
10
|
-
* Copyright 2020-
|
|
10
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
11
11
|
* SPDX-License-Identifier: Apache-2.0
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -16,6 +16,7 @@ var _console = require("./console");
|
|
|
16
16
|
* @typedef {object} ObfuscationRule
|
|
17
17
|
* @property {string|RegExp} regex The regular expression to match against in the payload
|
|
18
18
|
* @property {string} [replacement] The string to replace the matched regex with
|
|
19
|
+
* @property {string[]} [eventFilter] An optional list of event types to which this rule should be applied. If not provided, or an empty array, the rule will be applied to all events.
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -30,14 +31,33 @@ var _console = require("./console");
|
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
class Obfuscator {
|
|
33
|
-
|
|
34
|
+
/**
|
|
35
|
+
* @param {Object} agentRef - Reference to the agent instance
|
|
36
|
+
* @param {string} [eventType] - Optional event type this obfuscator instance handles.
|
|
37
|
+
* If provided, only rules matching this event type (or rules with no eventFilter) will be applied.
|
|
38
|
+
*/
|
|
39
|
+
constructor(agentRef, eventType) {
|
|
34
40
|
this.agentRef = agentRef;
|
|
41
|
+
this.eventType = eventType;
|
|
35
42
|
this.warnedRegexMissing = false;
|
|
36
43
|
this.warnedInvalidRegex = false;
|
|
37
44
|
this.warnedInvalidReplacement = false;
|
|
38
45
|
}
|
|
39
46
|
get obfuscateConfigRules() {
|
|
40
|
-
|
|
47
|
+
const allRules = this.agentRef.init.obfuscate || [];
|
|
48
|
+
|
|
49
|
+
// If this instance has no specific event type, return all rules
|
|
50
|
+
if (!this.eventType) return allRules;
|
|
51
|
+
|
|
52
|
+
// Filter rules to only those that apply to this instance's event type
|
|
53
|
+
return allRules.filter(rule => {
|
|
54
|
+
// If rule has no eventFilter, it applies to all events
|
|
55
|
+
if (!this.#hasValidEventFilter(rule)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
// Otherwise, check if this instance's event type matches the rule's filter
|
|
59
|
+
return rule.eventFilter.includes(this.eventType);
|
|
60
|
+
});
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
/**
|
|
@@ -63,6 +83,130 @@ class Obfuscator {
|
|
|
63
83
|
}, input);
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Traverses an object and obfuscates all string properties.
|
|
88
|
+
* This instance will only apply rules that match its configured event type (if any).
|
|
89
|
+
* For features with mixed event types in their payloads (like generic_events), this will
|
|
90
|
+
* traverse the object and check each object's eventType property against the rules.
|
|
91
|
+
* @param {Object|Array} obj - The object or array to traverse
|
|
92
|
+
* @returns {Object|Array} The modified object
|
|
93
|
+
*/
|
|
94
|
+
traverseAndObfuscateEvents(obj) {
|
|
95
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
96
|
+
|
|
97
|
+
// If this instance was configured with a specific event type, obfuscate everything
|
|
98
|
+
// (rules are already filtered in obfuscateConfigRules getter)
|
|
99
|
+
if (this.eventType) {
|
|
100
|
+
this.#applyFnToAllStrings(obj, this.obfuscateString.bind(this));
|
|
101
|
+
return obj;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// For generic obfuscators (no specific event types), check individual eventType properties
|
|
105
|
+
// This path is for features like generic_events that handle multiple event types
|
|
106
|
+
const eventTypesToObfuscate = new Set();
|
|
107
|
+
const globalRules = [];
|
|
108
|
+
const eventSpecificRules = [];
|
|
109
|
+
this.obfuscateConfigRules.forEach(rule => {
|
|
110
|
+
if (this.#hasValidEventFilter(rule)) {
|
|
111
|
+
eventSpecificRules.push(rule);
|
|
112
|
+
rule.eventFilter.forEach(eventType => eventTypesToObfuscate.add(eventType));
|
|
113
|
+
} else {
|
|
114
|
+
globalRules.push(rule);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Optimization: if ALL rules are global (no event-specific rules exist),
|
|
119
|
+
// we can just apply all rules to everything without checking eventType properties
|
|
120
|
+
if (eventSpecificRules.length === 0) {
|
|
121
|
+
this.#applyFnToAllStrings(obj, this.obfuscateString.bind(this));
|
|
122
|
+
return obj;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// We have a mix of global and event-specific rules:
|
|
126
|
+
// - Apply event-specific rules only to matching eventTypes
|
|
127
|
+
// - Apply global rules to ALL event types
|
|
128
|
+
this.#applyFnWithEventTypeFilter(obj, this.obfuscateString.bind(this), Array.from(eventTypesToObfuscate), globalRules, null);
|
|
129
|
+
return obj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Recursively applies a function to all string properties in an object.
|
|
134
|
+
* @param {Object|Array} obj - The object or array to traverse
|
|
135
|
+
* @param {Function} fn - The function to apply to string properties
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
#applyFnToAllStrings(obj, fn) {
|
|
139
|
+
if (!obj || typeof obj !== 'object') return;
|
|
140
|
+
Object.keys(obj).forEach(property => {
|
|
141
|
+
const value = obj[property];
|
|
142
|
+
if (typeof value === 'object' && value !== null) {
|
|
143
|
+
this.#applyFnToAllStrings(value, fn);
|
|
144
|
+
} else if (typeof value === 'string') {
|
|
145
|
+
obj[property] = fn(value);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Recursively applies a function to string properties based on eventType filtering.
|
|
152
|
+
* @param {Object|Array} obj - The object or array to traverse
|
|
153
|
+
* @param {Function} fn - The function to apply to string properties (applies all rules)
|
|
154
|
+
* @param {string[]} eventTypes - Array of event types to apply event-specific obfuscation
|
|
155
|
+
* @param {Array} globalRules - Rules without eventFilter that apply to all events
|
|
156
|
+
* @param {boolean|null} shouldObfuscate - Track obfuscation state: null = not determined yet, true = obfuscate with all rules, false = obfuscate with global rules only
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
#applyFnWithEventTypeFilter(obj, fn, eventTypes, globalRules, shouldObfuscate) {
|
|
160
|
+
if (!obj || typeof obj !== 'object') return;
|
|
161
|
+
|
|
162
|
+
// Determine the obfuscation state for this object
|
|
163
|
+
let currentShouldObfuscate = shouldObfuscate;
|
|
164
|
+
|
|
165
|
+
// Check if this object has an eventType property
|
|
166
|
+
if ('eventType' in obj && typeof obj.eventType === 'string') {
|
|
167
|
+
currentShouldObfuscate = eventTypes.includes(obj.eventType);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Process all properties
|
|
171
|
+
Object.keys(obj).forEach(property => {
|
|
172
|
+
const value = obj[property];
|
|
173
|
+
if (typeof value === 'object' && value !== null) {
|
|
174
|
+
// Recursively traverse objects/arrays, passing the current obfuscation state
|
|
175
|
+
this.#applyFnWithEventTypeFilter(value, fn, eventTypes, globalRules, currentShouldObfuscate);
|
|
176
|
+
} else if (typeof value === 'string') {
|
|
177
|
+
if (currentShouldObfuscate === true) {
|
|
178
|
+
// Apply all rules (both global and event-specific)
|
|
179
|
+
obj[property] = fn(value);
|
|
180
|
+
} else if (currentShouldObfuscate === false && globalRules.length > 0) {
|
|
181
|
+
// Apply only global rules
|
|
182
|
+
obj[property] = this.#applyRulesToString(value, globalRules);
|
|
183
|
+
}
|
|
184
|
+
// If currentShouldObfuscate is null, we haven't found an eventType yet, so don't obfuscate
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Applies specific obfuscation rules to a string.
|
|
191
|
+
* @param {string} input - The string to obfuscate
|
|
192
|
+
* @param {Array} rules - The rules to apply
|
|
193
|
+
* @returns {string} The obfuscated string
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
#applyRulesToString(input, rules) {
|
|
197
|
+
if (typeof input !== 'string' || input.trim().length === 0) return input;
|
|
198
|
+
const validatedRules = rules.map(rule => this.validateObfuscationRule(rule));
|
|
199
|
+
return validatedRules.filter(ruleValidation => ruleValidation.isValid).reduce((input, ruleValidation) => {
|
|
200
|
+
const {
|
|
201
|
+
rule
|
|
202
|
+
} = ruleValidation;
|
|
203
|
+
return input.replace(rule.regex, rule.replacement || '*');
|
|
204
|
+
}, input);
|
|
205
|
+
}
|
|
206
|
+
#hasValidEventFilter(rule) {
|
|
207
|
+
return Array.isArray(rule?.eventFilter) && rule.eventFilter.length > 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
66
210
|
/**
|
|
67
211
|
* Validates an obfuscation rule and provides errors if any are found.
|
|
68
212
|
* @param {ObfuscationRule} rule The rule to validate
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.trackMFEVitals = trackMFEVitals;
|
|
7
|
+
var _runtime = require("../constants/runtime");
|
|
8
|
+
var _now = require("../timing/now");
|
|
9
|
+
var _cleanUrl = require("../url/clean-url");
|
|
10
|
+
/**
|
|
11
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
12
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate a simple CSS selector for an element
|
|
21
|
+
* @param {Element} elem - The element to generate selector for
|
|
22
|
+
* @returns {string|null} CSS selector or null if element is invalid
|
|
23
|
+
*/
|
|
24
|
+
const getElementSelector = elem => {
|
|
25
|
+
try {
|
|
26
|
+
if (!elem || !elem.tagName) return null;
|
|
27
|
+
let selector = elem.tagName.toLowerCase();
|
|
28
|
+
if (elem.id) selector += "#".concat(elem.id);else if (elem.className && typeof elem.className === 'string') {
|
|
29
|
+
const classes = elem.className.trim().split(/\s+/).slice(0, 2).join('.');
|
|
30
|
+
if (classes) selector += ".".concat(classes);
|
|
31
|
+
}
|
|
32
|
+
return selector;
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract URL from an element (for images, videos, etc.)
|
|
40
|
+
* @param {Element} elem - The element to extract URL from
|
|
41
|
+
* @returns {string|null} Cleaned URL or null
|
|
42
|
+
*/
|
|
43
|
+
const getElementURL = elem => {
|
|
44
|
+
try {
|
|
45
|
+
if (!elem) return null;
|
|
46
|
+
|
|
47
|
+
// For images and videos
|
|
48
|
+
if (elem.src) return (0, _cleanUrl.cleanURL)(elem.src);
|
|
49
|
+
|
|
50
|
+
// For elements with background images
|
|
51
|
+
const bgImage = _runtime.globalScope.getComputedStyle?.(elem)?.backgroundImage;
|
|
52
|
+
if (bgImage && bgImage !== 'none') {
|
|
53
|
+
const match = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
|
|
54
|
+
if (match?.[1]) return (0, _cleanUrl.cleanURL)(match[1]);
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const isObservable = node => {
|
|
62
|
+
try {
|
|
63
|
+
return node?.textContent?.trim() || ['img', 'video', 'canvas', 'svg'].includes(node?.nodeName?.toLowerCase());
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if node is within a specific MFE
|
|
71
|
+
* @param {Node} node - DOM node to check
|
|
72
|
+
* @param {string} id - MFE ID to match
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
const isInMFE = (node, id) => {
|
|
76
|
+
try {
|
|
77
|
+
let curr = node.nodeType === 1 ? node : node.parentElement;
|
|
78
|
+
while (curr?.tagName) {
|
|
79
|
+
if (curr.dataset?.nrMfeId === id) return true;
|
|
80
|
+
curr = curr.parentNode;
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {}
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create mutation observer for MFE nodes
|
|
88
|
+
* @param {string} id - MFE ID to track
|
|
89
|
+
* @param {Function} onMatch - Callback when matching node is added
|
|
90
|
+
* @returns {MutationObserver}
|
|
91
|
+
*/
|
|
92
|
+
const observeMutations = (id, onMatch) => {
|
|
93
|
+
const obs = new _runtime.globalScope.MutationObserver(mutations => {
|
|
94
|
+
mutations.forEach(m => m.addedNodes.forEach(node => {
|
|
95
|
+
if (isObservable(node) && isInMFE(node, id)) {
|
|
96
|
+
onMatch(node, obs);
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
});
|
|
100
|
+
obs.observe(_runtime.globalScope.document, {
|
|
101
|
+
childList: true,
|
|
102
|
+
subtree: true
|
|
103
|
+
});
|
|
104
|
+
return obs;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create performance observer for MFE entries
|
|
109
|
+
* @param {Array} observers - Array to track observers
|
|
110
|
+
* @param {Object} config - Observer configuration
|
|
111
|
+
* @param {Function} onEntry - Callback for each entry
|
|
112
|
+
* @returns {PerformanceObserver|null} Observer if successful, null if unsupported
|
|
113
|
+
*/
|
|
114
|
+
const observePerformance = (observers, config, onEntry) => {
|
|
115
|
+
try {
|
|
116
|
+
const obs = new _runtime.globalScope.PerformanceObserver(list => {
|
|
117
|
+
list.getEntries().forEach(onEntry);
|
|
118
|
+
});
|
|
119
|
+
obs.observe(config);
|
|
120
|
+
observers.push(obs);
|
|
121
|
+
return obs;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Tracks all Core Web Vitals for a specific MFE.
|
|
129
|
+
* @param {string} id - The MFE ID to track
|
|
130
|
+
* @returns {{fcp: object|null, lcp: object|null, cls: object|null, inp: object|null, disconnect: Function}}
|
|
131
|
+
*/
|
|
132
|
+
function trackMFEVitals(id) {
|
|
133
|
+
const vitals = {
|
|
134
|
+
fcp: null,
|
|
135
|
+
lcp: null,
|
|
136
|
+
cls: null,
|
|
137
|
+
inp: null,
|
|
138
|
+
disconnect: () => {}
|
|
139
|
+
};
|
|
140
|
+
if (!id || !_runtime.isBrowserScope || !_runtime.globalScope.MutationObserver || !_runtime.globalScope.PerformanceObserver) return vitals;
|
|
141
|
+
const observers = [];
|
|
142
|
+
|
|
143
|
+
// Track FCP - first contentful paint
|
|
144
|
+
observeMutations(id, (node, obs) => {
|
|
145
|
+
if (vitals.fcp === null) {
|
|
146
|
+
vitals.fcp = {
|
|
147
|
+
value: (0, _now.now)(),
|
|
148
|
+
loadState: _runtime.globalScope.document?.readyState || null
|
|
149
|
+
};
|
|
150
|
+
obs.disconnect();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Track LCP - largest contentful paint
|
|
155
|
+
let largestSize = 0;
|
|
156
|
+
const lcpObs = observeMutations(id, node => {
|
|
157
|
+
try {
|
|
158
|
+
const elem = node.nodeType === 1 ? node : node.parentElement;
|
|
159
|
+
if (!elem) return;
|
|
160
|
+
const rect = elem.getBoundingClientRect();
|
|
161
|
+
const size = rect.width * rect.height;
|
|
162
|
+
if (size > largestSize) {
|
|
163
|
+
largestSize = size;
|
|
164
|
+
vitals.lcp = {
|
|
165
|
+
value: (0, _now.now)(),
|
|
166
|
+
size,
|
|
167
|
+
elTag: elem.tagName || null,
|
|
168
|
+
eid: elem.id || null,
|
|
169
|
+
elUrl: getElementURL(elem)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
// Element may be detached from DOM
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
observers.push(lcpObs);
|
|
177
|
+
|
|
178
|
+
// Track CLS - cumulative layout shift
|
|
179
|
+
// Initialize CLS to 0 if browser supports it AND the MFE container exists in the DOM
|
|
180
|
+
let clsValue = 0;
|
|
181
|
+
const clsObs = observePerformance(observers, {
|
|
182
|
+
type: 'layout-shift',
|
|
183
|
+
buffered: true
|
|
184
|
+
}, entry => {
|
|
185
|
+
if (entry.hadRecentInput || !vitals.cls) return;
|
|
186
|
+
(entry.sources || []).some(source => {
|
|
187
|
+
if (isInMFE(source.node, id)) {
|
|
188
|
+
clsValue += entry.value;
|
|
189
|
+
vitals.cls ??= {
|
|
190
|
+
value: 0,
|
|
191
|
+
largestShiftValue: null,
|
|
192
|
+
largestShiftTime: null,
|
|
193
|
+
largestShiftTarget: null,
|
|
194
|
+
loadState: null
|
|
195
|
+
};
|
|
196
|
+
vitals.cls.value = clsValue;
|
|
197
|
+
|
|
198
|
+
// Track largest shift
|
|
199
|
+
if (entry.value > (vitals.cls.largestShiftValue || 0)) {
|
|
200
|
+
vitals.cls.largestShiftValue = entry.value;
|
|
201
|
+
vitals.cls.largestShiftTime = entry.startTime;
|
|
202
|
+
vitals.cls.largestShiftTarget = getElementSelector(source.node);
|
|
203
|
+
vitals.cls.loadState = _runtime.globalScope.document?.readyState || null;
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
if (clsObs) {
|
|
211
|
+
try {
|
|
212
|
+
const mfeContainer = _runtime.globalScope.document?.querySelector("[data-nr-mfe-id=\"".concat(id, "\"]"));
|
|
213
|
+
if (mfeContainer) {
|
|
214
|
+
vitals.cls ??= {
|
|
215
|
+
value: 0,
|
|
216
|
+
largestShiftValue: null,
|
|
217
|
+
largestShiftTime: null,
|
|
218
|
+
largestShiftTarget: null,
|
|
219
|
+
loadState: null
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
// querySelector may fail, leave CLS as null
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Track INP - interaction to next paint
|
|
228
|
+
observePerformance(observers, {
|
|
229
|
+
type: 'event',
|
|
230
|
+
buffered: true,
|
|
231
|
+
durationThreshold: 16
|
|
232
|
+
}, entry => {
|
|
233
|
+
if (!entry.interactionId || !entry.target || !isInMFE(entry.target, id)) return;
|
|
234
|
+
if (vitals.inp === null || entry.duration > vitals.inp.value) {
|
|
235
|
+
vitals.inp = {
|
|
236
|
+
value: entry.duration,
|
|
237
|
+
interactionTarget: getElementSelector(entry.target),
|
|
238
|
+
interactionTime: entry.startTime,
|
|
239
|
+
interactionType: entry.name,
|
|
240
|
+
inputDelay: entry.processingStart ? entry.processingStart - entry.startTime : null,
|
|
241
|
+
processingDuration: entry.processingStart && entry.processingEnd ? entry.processingEnd - entry.processingStart : null,
|
|
242
|
+
presentationDelay: entry.processingEnd && entry.duration ? entry.duration - (entry.processingEnd - entry.startTime) : null,
|
|
243
|
+
nextPaintTime: entry.startTime + entry.duration,
|
|
244
|
+
loadState: _runtime.globalScope.document?.readyState || null
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Disconnect all observers
|
|
250
|
+
vitals.disconnect = () => {
|
|
251
|
+
// Disconnect all observers
|
|
252
|
+
observers.forEach(obs => {
|
|
253
|
+
try {
|
|
254
|
+
obs?.disconnect();
|
|
255
|
+
} catch (e) {
|
|
256
|
+
// Observer may already be disconnected
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Auto-disconnect LCP observer on user interaction (per Web Vitals spec)
|
|
262
|
+
// CLS and INP continue tracking until visibility change or deregister
|
|
263
|
+
const disconnectLCP = () => {
|
|
264
|
+
try {
|
|
265
|
+
lcpObs?.disconnect();
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// Observer may already be disconnected
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
['click', 'keydown', 'scroll'].forEach(type => {
|
|
271
|
+
_runtime.globalScope.addEventListener(type, disconnectLCP, {
|
|
272
|
+
once: true,
|
|
273
|
+
passive: true
|
|
274
|
+
});
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
// Disconnect all observers on visibility change or page unload
|
|
278
|
+
;
|
|
279
|
+
['visibilitychange', 'pagehide'].forEach(type => {
|
|
280
|
+
_runtime.globalScope.addEventListener(type, vitals.disconnect, {
|
|
281
|
+
once: true,
|
|
282
|
+
passive: true
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
return vitals;
|
|
286
|
+
}
|