@newrelic/browser-agent 1.316.0 → 1.317.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/cjs/common/config/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 +12 -4
- package/dist/cjs/common/payloads/payloads.js +127 -0
- package/dist/cjs/common/serialize/bel-serializer.js +4 -3
- package/dist/cjs/common/util/data-size.js +2 -2
- package/dist/cjs/common/util/obfuscate.js +147 -3
- 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 +7 -3
- package/dist/cjs/features/metrics/aggregate/index.js +5 -1
- package/dist/cjs/features/page_view_event/aggregate/index.js +7 -3
- package/dist/cjs/features/page_view_timing/aggregate/index.js +6 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +7 -1
- package/dist/cjs/features/session_trace/aggregate/index.js +6 -2
- 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 +2 -3
- 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 +12 -4
- package/dist/esm/common/payloads/payloads.js +118 -0
- package/dist/esm/common/serialize/bel-serializer.js +4 -3
- package/dist/esm/common/util/data-size.js +2 -2
- package/dist/esm/common/util/obfuscate.js +147 -3
- 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 +7 -3
- package/dist/esm/features/metrics/aggregate/index.js +5 -1
- package/dist/esm/features/page_view_event/aggregate/index.js +7 -3
- package/dist/esm/features/page_view_timing/aggregate/index.js +6 -1
- package/dist/esm/features/session_replay/aggregate/index.js +7 -1
- package/dist/esm/features/session_trace/aggregate/index.js +6 -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 +2 -3
- 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 +4 -1
- package/dist/types/common/harvest/harvester.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/util/obfuscate.d.ts +23 -2
- package/dist/types/common/util/obfuscate.d.ts.map +1 -1
- 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 +2 -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 +2 -0
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +2 -0
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- 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/package.json +2 -2
- 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 +11 -4
- package/src/common/payloads/payloads.js +135 -0
- package/src/common/serialize/bel-serializer.js +4 -3
- package/src/common/util/data-size.js +2 -2
- package/src/common/util/obfuscate.js +154 -3
- 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 +7 -6
- package/src/features/metrics/aggregate/index.js +5 -1
- package/src/features/page_view_event/aggregate/index.js +6 -2
- package/src/features/page_view_timing/aggregate/index.js +6 -1
- package/src/features/session_replay/aggregate/index.js +7 -1
- package/src/features/session_trace/aggregate/index.js +6 -2
- 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 +1 -3
|
@@ -16,6 +16,7 @@ import { warn } from '../util/console'
|
|
|
16
16
|
import { stringify } from '../util/stringify'
|
|
17
17
|
import { getSubmitMethod, xhr as xhrMethod, xhrFetch as fetchMethod } from '../util/submit-data'
|
|
18
18
|
import { dispatchGlobalEvent } from '../dispatch/global-event'
|
|
19
|
+
import { Obfuscator } from '../util/obfuscate'
|
|
19
20
|
|
|
20
21
|
const RETRY = 'Harvester/Retry/'
|
|
21
22
|
const RETRY_ATTEMPTED = RETRY + 'Attempted/'
|
|
@@ -29,6 +30,10 @@ export class Harvester {
|
|
|
29
30
|
constructor (agentRef) {
|
|
30
31
|
this.agentRef = agentRef
|
|
31
32
|
|
|
33
|
+
// Create obfuscator for harvest metadata (referrer URL, etc.)
|
|
34
|
+
// No event type specified - applies to all harvests regardless of feature
|
|
35
|
+
this.obfuscator = new Obfuscator(agentRef)
|
|
36
|
+
|
|
32
37
|
subscribeToEOL(() => { // do one last harvest round or check
|
|
33
38
|
this.initializedAggregates.forEach(aggregateInst => { // let all features wrap up things needed to do before ANY harvest in case there's last minute cross-feature data dependencies
|
|
34
39
|
if (typeof aggregateInst.harvestOpts.beforeUnload === 'function') aggregateInst.harvestOpts.beforeUnload()
|
|
@@ -73,6 +78,7 @@ export class Harvester {
|
|
|
73
78
|
payload: output.payload,
|
|
74
79
|
localOpts,
|
|
75
80
|
submitMethod,
|
|
81
|
+
harvesterObfuscator: this.obfuscator,
|
|
76
82
|
cbFinished,
|
|
77
83
|
raw: aggregateInst.harvestOpts.raw,
|
|
78
84
|
featureName: aggregateInst.featureName,
|
|
@@ -112,7 +118,7 @@ const warnings = {}
|
|
|
112
118
|
* @param {NetworkSendSpec} param0 Specification for sending data
|
|
113
119
|
* @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
|
|
114
120
|
*/
|
|
115
|
-
export function send (agentRef, { endpoint, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName, endpointVersion = 1 }) {
|
|
121
|
+
export function send (agentRef, { endpoint, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName, endpointVersion = 1, harvesterObfuscator }) {
|
|
116
122
|
if (!agentRef.info.errorBeacon) return false
|
|
117
123
|
|
|
118
124
|
let { body, qs } = cleanPayload(payload)
|
|
@@ -127,7 +133,7 @@ export function send (agentRef, { endpoint, payload, localOpts = {}, submitMetho
|
|
|
127
133
|
const url = raw
|
|
128
134
|
? `${protocol}://${perceivedBeacon}/${endpoint}`
|
|
129
135
|
: `${protocol}://${perceivedBeacon}${endpoint !== RUM ? '/' + endpoint : ''}/${endpointVersion}/${agentRef.info.licenseKey}`
|
|
130
|
-
const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint) : ''
|
|
136
|
+
const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) : ''
|
|
131
137
|
let payloadParams = obj(qs, agentRef.runtime.maxBytes)
|
|
132
138
|
if (baseParams === '' && payloadParams.startsWith('&')) {
|
|
133
139
|
payloadParams = payloadParams.substring(1)
|
|
@@ -260,8 +266,9 @@ function cleanPayload (payload = {}) {
|
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
// The stuff that gets sent every time.
|
|
263
|
-
function baseQueryString (agentRef, qs, endpoint) {
|
|
264
|
-
const
|
|
269
|
+
function baseQueryString (agentRef, qs, endpoint, harvesterObfuscator) {
|
|
270
|
+
const cleanedURL = cleanURL('' + globalScope.location)
|
|
271
|
+
const ref = harvesterObfuscator?.obfuscateString(cleanedURL) ?? cleanedURL
|
|
265
272
|
const session = agentRef.runtime.session
|
|
266
273
|
const hr = !!session?.state.sessionReplaySentFirstChunk && session?.state.sessionReplayMode === 1 && endpoint !== JSERRORS
|
|
267
274
|
const ht = !!session?.state.traceHarvestStarted && session?.state.sessionTraceMode === 1 && ![LOGS, BLOBS].includes(endpoint)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { stringify } from '../util/stringify'
|
|
7
|
+
import { CAPTURE_PAYLOAD_SETTINGS } from '../../features/ajax/constants'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Determines whether payload data should be captured based on the capture mode setting,
|
|
11
|
+
* HTTP status code, and GraphQL error status.
|
|
12
|
+
* @param {string} captureMode - The capture mode setting ('none', 'all', or 'failures')
|
|
13
|
+
* @param {number} statusCode - The HTTP status code
|
|
14
|
+
* @param {boolean} hasGQLErrors - Whether the response contains GraphQL errors
|
|
15
|
+
* @returns {boolean} True if payload should be captured
|
|
16
|
+
*/
|
|
17
|
+
export function canCapturePayload (captureMode, statusCode, hasGQLErrors) {
|
|
18
|
+
if (captureMode === CAPTURE_PAYLOAD_SETTINGS.ALL) return true
|
|
19
|
+
if (!captureMode || captureMode === CAPTURE_PAYLOAD_SETTINGS.NONE) return false
|
|
20
|
+
|
|
21
|
+
// Default "failures" mode
|
|
22
|
+
return statusCode === 0 || statusCode >= 400 || hasGQLErrors === true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parses a query string into an object of key-value pairs.
|
|
27
|
+
* @param {string} search a query string starting with "?" or without (ex. new URL(...).search)
|
|
28
|
+
* @returns {Object|undefined} Parsed query parameters as key-value pairs. Returns undefined if no valid parameters are found.
|
|
29
|
+
*/
|
|
30
|
+
export function parseQueryString (search) {
|
|
31
|
+
if (!search || search.length === 0) return
|
|
32
|
+
|
|
33
|
+
const queryParams = {}
|
|
34
|
+
try {
|
|
35
|
+
const searchParams = new URLSearchParams(search)
|
|
36
|
+
searchParams.forEach(function (value, key) {
|
|
37
|
+
queryParams[key] = value
|
|
38
|
+
})
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Fallback for environments without URLSearchParams
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return queryParams
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Determines if the given content type is likely to be human-readable (text-based).
|
|
48
|
+
* @param {Object} headers - The headers object containing content-type
|
|
49
|
+
* @param {*} data - The data to check
|
|
50
|
+
* @returns {boolean} True if the content type is human-readable (text-based)
|
|
51
|
+
*/
|
|
52
|
+
export function isLikelyHumanReadable (headers, data) {
|
|
53
|
+
if (!headers) return typeof data === 'string'
|
|
54
|
+
var contentType = headers['content-type']
|
|
55
|
+
if (!contentType) return typeof data === 'string'
|
|
56
|
+
// Normalize to lowercase and extract the mime type (ignore charset, etc.)
|
|
57
|
+
var mimeType = contentType.toLowerCase().split(';')[0].trim()
|
|
58
|
+
|
|
59
|
+
// Check for text/* types
|
|
60
|
+
if (mimeType.indexOf('text/') === 0) return true
|
|
61
|
+
|
|
62
|
+
// Check for specific application/* types
|
|
63
|
+
var readableAppTypes = [
|
|
64
|
+
'/json',
|
|
65
|
+
'/xml',
|
|
66
|
+
'/xhtml+xml',
|
|
67
|
+
'/ld+json',
|
|
68
|
+
'/yaml',
|
|
69
|
+
'/x-www-form-urlencoded'
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for (var i = 0; i < readableAppTypes.length; i++) {
|
|
73
|
+
if (mimeType === 'application' + readableAppTypes[i]) return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Truncates a string to ensure its UTF-8 byte length does not exceed 4092 bytes. If truncation is necessary,
|
|
81
|
+
* the string is cut off at a character boundary to avoid breaking multi-byte characters and " ..." is appended to indicate truncation.
|
|
82
|
+
* If not a string, it is first converted to a string using JSON.stringify.
|
|
83
|
+
* @param {*} data The data to truncate.
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export function truncateAsString (data) {
|
|
87
|
+
if (!data) return data
|
|
88
|
+
try {
|
|
89
|
+
if (typeof data !== 'string') data = stringify(data)
|
|
90
|
+
let bytes = 0
|
|
91
|
+
let i = 0
|
|
92
|
+
let needsEllipsis = false
|
|
93
|
+
|
|
94
|
+
while (i < data.length) {
|
|
95
|
+
const c = data.charCodeAt(i)
|
|
96
|
+
const charBytes = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0xD800 || c >= 0xE000 ? 3 : 4
|
|
97
|
+
|
|
98
|
+
if (bytes + charBytes > 4092) {
|
|
99
|
+
needsEllipsis = true
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
bytes += charBytes
|
|
104
|
+
i += charBytes === 4 ? 2 : 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return data.slice(0, i) + (needsEllipsis ? ' ...' : '')
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return data
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates string adder functions for BEL serialization with obfuscation and optional truncation.
|
|
115
|
+
* This ensures a single string table is used while providing separate handling for regular vs payload attributes.
|
|
116
|
+
* @param {Function} getAddStringContext - Function that creates a new string table context
|
|
117
|
+
* @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
|
|
118
|
+
* @returns {{addString: Function, addStringWithTruncation: Function}} Object containing both string adder functions
|
|
119
|
+
*/
|
|
120
|
+
export function createStringAdders (getAddStringContext, obfuscator) {
|
|
121
|
+
const addStringRaw = getAddStringContext()
|
|
122
|
+
|
|
123
|
+
const processString = (str, shouldTruncate) => {
|
|
124
|
+
if (typeof str === 'undefined' || str === '') return addStringRaw(str)
|
|
125
|
+
if (typeof str !== 'string') str = stringify(str)
|
|
126
|
+
const obfuscated = obfuscator?.obfuscateString(str) ?? str
|
|
127
|
+
const processed = shouldTruncate ? truncateAsString(obfuscated) : obfuscated
|
|
128
|
+
return addStringRaw(processed)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
addString: (str) => processString(str, false),
|
|
133
|
+
addStringWithTruncation: (str) => processString(str, true)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -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
|
|
|
@@ -21,7 +21,7 @@ export function numeric (n, noDefault) {
|
|
|
21
21
|
return (n === undefined || n === 0) ? '' : Math.floor(n).toString(36)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function getAddStringContext (obfuscator) {
|
|
24
|
+
export function getAddStringContext (obfuscator, truncator) {
|
|
25
25
|
let stringTableIdx = 0
|
|
26
26
|
const stringTable = Object.prototype.hasOwnProperty.call(Object, 'create') ? Object.create(null) : {}
|
|
27
27
|
|
|
@@ -29,7 +29,8 @@ export function getAddStringContext (obfuscator) {
|
|
|
29
29
|
|
|
30
30
|
function addString (str) {
|
|
31
31
|
if (typeof str === 'undefined' || str === '') return ''
|
|
32
|
-
str = obfuscator
|
|
32
|
+
str = obfuscator?.obfuscateString(String(str)) ?? String(str)
|
|
33
|
+
str = truncator?.(str) ?? str
|
|
33
34
|
if (hasOwnProp.call(stringTable, str)) {
|
|
34
35
|
return numeric(stringTable[str], true)
|
|
35
36
|
} else {
|
|
@@ -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
|
|
|
@@ -12,7 +12,7 @@ import { stringify } from './stringify'
|
|
|
12
12
|
* @returns {(number|undefined)} - The size of the data or undefined if size cannot be determined.
|
|
13
13
|
*/
|
|
14
14
|
export function dataSize (data) {
|
|
15
|
-
if (typeof data === 'string'
|
|
15
|
+
if (typeof data === 'string') return data.length
|
|
16
16
|
if (typeof data !== 'object') return undefined
|
|
17
17
|
// eslint-disable-next-line
|
|
18
18
|
if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer && data.byteLength) return data.byteLength
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2020-
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import { isFileProtocol } from '../url/protocol'
|
|
@@ -10,6 +10,7 @@ import { warn } from './console'
|
|
|
10
10
|
* @typedef {object} ObfuscationRule
|
|
11
11
|
* @property {string|RegExp} regex The regular expression to match against in the payload
|
|
12
12
|
* @property {string} [replacement] The string to replace the matched regex with
|
|
13
|
+
* @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.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -24,15 +25,34 @@ import { warn } from './console'
|
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
export class Obfuscator {
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} agentRef - Reference to the agent instance
|
|
30
|
+
* @param {string} [eventType] - Optional event type this obfuscator instance handles.
|
|
31
|
+
* If provided, only rules matching this event type (or rules with no eventFilter) will be applied.
|
|
32
|
+
*/
|
|
33
|
+
constructor (agentRef, eventType) {
|
|
28
34
|
this.agentRef = agentRef
|
|
35
|
+
this.eventType = eventType
|
|
29
36
|
this.warnedRegexMissing = false
|
|
30
37
|
this.warnedInvalidRegex = false
|
|
31
38
|
this.warnedInvalidReplacement = false
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
get obfuscateConfigRules () {
|
|
35
|
-
|
|
42
|
+
const allRules = this.agentRef.init.obfuscate || []
|
|
43
|
+
|
|
44
|
+
// If this instance has no specific event type, return all rules
|
|
45
|
+
if (!this.eventType) return allRules
|
|
46
|
+
|
|
47
|
+
// Filter rules to only those that apply to this instance's event type
|
|
48
|
+
return allRules.filter(rule => {
|
|
49
|
+
// If rule has no eventFilter, it applies to all events
|
|
50
|
+
if (!this.#hasValidEventFilter(rule)) {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
// Otherwise, check if this instance's event type matches the rule's filter
|
|
54
|
+
return rule.eventFilter.includes(this.eventType)
|
|
55
|
+
})
|
|
36
56
|
}
|
|
37
57
|
|
|
38
58
|
/**
|
|
@@ -60,6 +80,137 @@ export class Obfuscator {
|
|
|
60
80
|
}, input)
|
|
61
81
|
}
|
|
62
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Traverses an object and obfuscates all string properties.
|
|
85
|
+
* This instance will only apply rules that match its configured event type (if any).
|
|
86
|
+
* For features with mixed event types in their payloads (like generic_events), this will
|
|
87
|
+
* traverse the object and check each object's eventType property against the rules.
|
|
88
|
+
* @param {Object|Array} obj - The object or array to traverse
|
|
89
|
+
* @returns {Object|Array} The modified object
|
|
90
|
+
*/
|
|
91
|
+
traverseAndObfuscateEvents (obj) {
|
|
92
|
+
if (!obj || typeof obj !== 'object') return obj
|
|
93
|
+
|
|
94
|
+
// If this instance was configured with a specific event type, obfuscate everything
|
|
95
|
+
// (rules are already filtered in obfuscateConfigRules getter)
|
|
96
|
+
if (this.eventType) {
|
|
97
|
+
this.#applyFnToAllStrings(obj, this.obfuscateString.bind(this))
|
|
98
|
+
return obj
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// For generic obfuscators (no specific event types), check individual eventType properties
|
|
102
|
+
// This path is for features like generic_events that handle multiple event types
|
|
103
|
+
const eventTypesToObfuscate = new Set()
|
|
104
|
+
const globalRules = []
|
|
105
|
+
const eventSpecificRules = []
|
|
106
|
+
|
|
107
|
+
this.obfuscateConfigRules.forEach(rule => {
|
|
108
|
+
if (this.#hasValidEventFilter(rule)) {
|
|
109
|
+
eventSpecificRules.push(rule)
|
|
110
|
+
rule.eventFilter.forEach(eventType => eventTypesToObfuscate.add(eventType))
|
|
111
|
+
} else {
|
|
112
|
+
globalRules.push(rule)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Optimization: if ALL rules are global (no event-specific rules exist),
|
|
117
|
+
// we can just apply all rules to everything without checking eventType properties
|
|
118
|
+
if (eventSpecificRules.length === 0) {
|
|
119
|
+
this.#applyFnToAllStrings(obj, this.obfuscateString.bind(this))
|
|
120
|
+
return obj
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// We have a mix of global and event-specific rules:
|
|
124
|
+
// - Apply event-specific rules only to matching eventTypes
|
|
125
|
+
// - Apply global rules to ALL event types
|
|
126
|
+
this.#applyFnWithEventTypeFilter(obj, this.obfuscateString.bind(this), Array.from(eventTypesToObfuscate), globalRules, null)
|
|
127
|
+
return obj
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Recursively applies a function to all string properties in an object.
|
|
132
|
+
* @param {Object|Array} obj - The object or array to traverse
|
|
133
|
+
* @param {Function} fn - The function to apply to string properties
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
#applyFnToAllStrings (obj, fn) {
|
|
137
|
+
if (!obj || typeof obj !== 'object') return
|
|
138
|
+
|
|
139
|
+
Object.keys(obj).forEach(property => {
|
|
140
|
+
const value = obj[property]
|
|
141
|
+
|
|
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
|
+
|
|
174
|
+
if (typeof value === 'object' && value !== null) {
|
|
175
|
+
// Recursively traverse objects/arrays, passing the current obfuscation state
|
|
176
|
+
this.#applyFnWithEventTypeFilter(value, fn, eventTypes, globalRules, currentShouldObfuscate)
|
|
177
|
+
} else if (typeof value === 'string') {
|
|
178
|
+
if (currentShouldObfuscate === true) {
|
|
179
|
+
// Apply all rules (both global and event-specific)
|
|
180
|
+
obj[property] = fn(value)
|
|
181
|
+
} else if (currentShouldObfuscate === false && globalRules.length > 0) {
|
|
182
|
+
// Apply only global rules
|
|
183
|
+
obj[property] = this.#applyRulesToString(value, globalRules)
|
|
184
|
+
}
|
|
185
|
+
// If currentShouldObfuscate is null, we haven't found an eventType yet, so don't obfuscate
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Applies specific obfuscation rules to a string.
|
|
192
|
+
* @param {string} input - The string to obfuscate
|
|
193
|
+
* @param {Array} rules - The rules to apply
|
|
194
|
+
* @returns {string} The obfuscated string
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
#applyRulesToString (input, rules) {
|
|
198
|
+
if (typeof input !== 'string' || input.trim().length === 0) return input
|
|
199
|
+
|
|
200
|
+
const validatedRules = rules.map(rule => this.validateObfuscationRule(rule))
|
|
201
|
+
|
|
202
|
+
return validatedRules
|
|
203
|
+
.filter(ruleValidation => ruleValidation.isValid)
|
|
204
|
+
.reduce((input, ruleValidation) => {
|
|
205
|
+
const { rule } = ruleValidation
|
|
206
|
+
return input.replace(rule.regex, rule.replacement || '*')
|
|
207
|
+
}, input)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#hasValidEventFilter (rule) {
|
|
211
|
+
return Array.isArray(rule?.eventFilter) && rule.eventFilter.length > 0
|
|
212
|
+
}
|
|
213
|
+
|
|
63
214
|
/**
|
|
64
215
|
* Validates an obfuscation rule and provides errors if any are found.
|
|
65
216
|
* @param {ObfuscationRule} rule The rule to validate
|
|
@@ -8,6 +8,7 @@ import { now } from '../timing/now'
|
|
|
8
8
|
import { cleanURL } from '../url/clean-url'
|
|
9
9
|
import { gosNREUMOriginals } from '../window/nreum'
|
|
10
10
|
import { subscribeToPageUnload } from '../window/page-visibility'
|
|
11
|
+
import { EVENT_TYPES } from '../constants/events'
|
|
11
12
|
|
|
12
13
|
const wrapped = {}
|
|
13
14
|
const openWebSockets = new Set() // track all instances to close out metrics on page unload
|
|
@@ -37,7 +38,7 @@ export function wrapWebSocket (sharedEE) {
|
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
class WrappedWebSocket extends WebSocket {
|
|
40
|
-
static name =
|
|
41
|
+
static name = EVENT_TYPES.WS
|
|
41
42
|
static toString () { // fake native WebSocket when static class is stringified
|
|
42
43
|
return 'function WebSocket() { [native code] }'
|
|
43
44
|
}
|
|
@@ -17,7 +17,7 @@ import { warn } from '../util/console'
|
|
|
17
17
|
import { findTargetsFromStackTrace } from '../v2/utils'
|
|
18
18
|
|
|
19
19
|
const wrapped = {}
|
|
20
|
-
const XHR_PROPS = ['open', 'send'] // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
|
|
20
|
+
const XHR_PROPS = ['open', 'send', 'setRequestHeader'] // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Wraps the native XMLHttpRequest (XHR) object to emit custom events to its readystatechange event and an assortment
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2020-
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import { isPureObject } from '../../../common/util/type-check'
|
|
@@ -97,3 +97,44 @@ function parseGQLQueryString (gqlQueryString) {
|
|
|
97
97
|
function validateGQLObject (obj) {
|
|
98
98
|
return !(typeof obj !== 'object' || !obj.query || typeof obj.query !== 'string')
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Checks if a response object has valid GraphQL errors.
|
|
103
|
+
* @param {object} response - A single GraphQL response object
|
|
104
|
+
* @returns {boolean} True if the response has valid errors
|
|
105
|
+
*/
|
|
106
|
+
function hasValidGQLErrors (response) {
|
|
107
|
+
return Array.isArray(response?.errors) &&
|
|
108
|
+
response.errors.some(err =>
|
|
109
|
+
err && typeof err === 'object' && typeof err.message === 'string'
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Checks if a response body contains GraphQL errors according to the GraphQL spec.
|
|
115
|
+
* A valid GraphQL error response contains an "errors" array with at least one error object.
|
|
116
|
+
* Supports both single and batched GraphQL responses.
|
|
117
|
+
* @param {string|object|array} [responseBody] The response body to check
|
|
118
|
+
* @returns {boolean} True if the response contains GraphQL errors
|
|
119
|
+
*/
|
|
120
|
+
export function hasGQLErrors (responseBody) {
|
|
121
|
+
if (!responseBody) return false
|
|
122
|
+
try {
|
|
123
|
+
let parsed = responseBody
|
|
124
|
+
|
|
125
|
+
// Parse string to object if needed
|
|
126
|
+
if (typeof responseBody === 'string') {
|
|
127
|
+
parsed = JSON.parse(responseBody)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Handle batched GraphQL responses (array of response objects)
|
|
131
|
+
if (Array.isArray(parsed)) {
|
|
132
|
+
return parsed.some(hasValidGQLErrors)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Handle single GraphQL response
|
|
136
|
+
return hasValidGQLErrors(parsed)
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -6,13 +6,16 @@ import { registerHandler } from '../../../common/event-emitter/register-handler'
|
|
|
6
6
|
import { stringify } from '../../../common/util/stringify'
|
|
7
7
|
import { handle } from '../../../common/event-emitter/handle'
|
|
8
8
|
import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list'
|
|
9
|
-
import {
|
|
9
|
+
import { FEATURE_NAME, AJAX_ID } from '../constants'
|
|
10
10
|
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
11
11
|
import { AggregateBase } from '../../utils/aggregate-base'
|
|
12
|
-
import { parseGQL } from './gql'
|
|
13
12
|
import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
|
|
14
13
|
import { gosNREUMOriginals } from '../../../common/window/nreum'
|
|
14
|
+
import { hasGQLErrors, parseGQL } from './gql'
|
|
15
|
+
import { canCapturePayload, isLikelyHumanReadable, parseQueryString, createStringAdders } from '../../../common/payloads/payloads'
|
|
16
|
+
import { Obfuscator } from '../../../common/util/obfuscate'
|
|
15
17
|
import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils'
|
|
18
|
+
import { EVENT_TYPES } from '../../../common/constants/events'
|
|
16
19
|
import { generateUuid } from '../../../common/ids/unique-id'
|
|
17
20
|
|
|
18
21
|
export class Aggregate extends AggregateBase {
|
|
@@ -23,6 +26,9 @@ export class Aggregate extends AggregateBase {
|
|
|
23
26
|
setDenyList(agentRef.runtime.denyList)
|
|
24
27
|
const classThis = this
|
|
25
28
|
|
|
29
|
+
// Create obfuscator for AJAX requests
|
|
30
|
+
this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.AJAX)
|
|
31
|
+
|
|
26
32
|
if (!agentRef.init.ajax.block_internal) {
|
|
27
33
|
// if the agent is tracking ITSELF, it can spawn endless ajax requests early if they are large from custom attributes, so we just disable early harvest for ajax in this case.
|
|
28
34
|
super.canHarvestEarly = false
|
|
@@ -96,6 +102,24 @@ export class Aggregate extends AggregateBase {
|
|
|
96
102
|
[AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
event.gql = params.gql = parseGQL({
|
|
106
|
+
body: ctx.requestBody,
|
|
107
|
+
query: ctx.parsedOrigin?.search
|
|
108
|
+
})
|
|
109
|
+
if (event.gql) event.gql.operationHasErrors = params.gql.operationHasErrors = hasGQLErrors(ctx.responseBody)
|
|
110
|
+
|
|
111
|
+
const capturePayloadSetting = this.agentRef.init.ajax.capture_payloads
|
|
112
|
+
const shouldCapturePayload = canCapturePayload(capturePayloadSetting, params.status, event.gql?.operationHasErrors)
|
|
113
|
+
|
|
114
|
+
if (shouldCapturePayload) {
|
|
115
|
+
// Store raw data; obfuscation and truncation will happen in the serializer
|
|
116
|
+
params.requestQuery = event.requestQuery = parseQueryString(ctx.parsedOrigin?.search)
|
|
117
|
+
params.requestHeaders = event.requestHeaders = ctx.requestHeaders
|
|
118
|
+
params.responseHeaders = event.responseHeaders = ctx.responseHeaders
|
|
119
|
+
if (isLikelyHumanReadable(ctx.requestHeaders, ctx.requestBody)) params.requestBody = event.requestBody = ctx.requestBody
|
|
120
|
+
if (isLikelyHumanReadable(ctx.responseHeaders, ctx.responseBody)) params.responseBody = event.responseBody = ctx.responseBody
|
|
121
|
+
}
|
|
122
|
+
|
|
99
123
|
if (ctx.dt) {
|
|
100
124
|
event.spanId = ctx.dt.spanId
|
|
101
125
|
event.traceId = ctx.dt.traceId
|
|
@@ -104,11 +128,6 @@ export class Aggregate extends AggregateBase {
|
|
|
104
128
|
)
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
// parsed from the AJAX body, looking for operationName param & parsing query for operationType
|
|
108
|
-
event.gql = params.gql = parseGQL({
|
|
109
|
-
body: ctx.body,
|
|
110
|
-
query: ctx.parsedOrigin?.search
|
|
111
|
-
})
|
|
112
131
|
if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length)
|
|
113
132
|
|
|
114
133
|
/** make a copy of the event for the MFE target if it exists */
|
|
@@ -131,7 +150,9 @@ export class Aggregate extends AggregateBase {
|
|
|
131
150
|
|
|
132
151
|
serializer (eventBuffer) {
|
|
133
152
|
if (!eventBuffer.length) return
|
|
134
|
-
|
|
153
|
+
|
|
154
|
+
const { addString, addStringWithTruncation } = createStringAdders(getAddStringContext, this.obfuscator)
|
|
155
|
+
|
|
135
156
|
let payload = 'bel.7;'
|
|
136
157
|
|
|
137
158
|
let firstTimestamp = 0
|
|
@@ -165,15 +186,24 @@ export class Aggregate extends AggregateBase {
|
|
|
165
186
|
// Since configuration objects (like info) are created new each time they are set, we have to grab the current pointer to the attr object here.
|
|
166
187
|
const jsAttributes = this.agentRef.info.jsAttributes
|
|
167
188
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
const attrParts = addCustomAttributes({
|
|
189
|
+
// Regular attributes: obfuscate only
|
|
190
|
+
const regularAttrs = addCustomAttributes({
|
|
171
191
|
...(jsAttributes || {}),
|
|
172
|
-
|
|
192
|
+
[AJAX_ID]: event[AJAX_ID], // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
173
193
|
...(event.targetAttributes || {}), // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
|
|
174
|
-
|
|
194
|
+
...(event.gql || {})
|
|
175
195
|
}, addString)
|
|
176
196
|
|
|
197
|
+
// Payload attributes: obfuscate then truncate
|
|
198
|
+
const payloadAttrs = addCustomAttributes({
|
|
199
|
+
...(event.requestBody ? { requestBody: event.requestBody } : {}),
|
|
200
|
+
...(event.requestHeaders ? { requestHeaders: event.requestHeaders } : {}),
|
|
201
|
+
...(event.requestQuery ? { requestQuery: event.requestQuery } : {}),
|
|
202
|
+
...(event.responseBody ? { responseBody: event.responseBody } : {}),
|
|
203
|
+
...(event.responseHeaders ? { responseHeaders: event.responseHeaders } : {})
|
|
204
|
+
}, addStringWithTruncation)
|
|
205
|
+
|
|
206
|
+
const attrParts = [...regularAttrs, ...payloadAttrs]
|
|
177
207
|
fields.unshift(numeric(attrParts.length))
|
|
178
208
|
|
|
179
209
|
insert += fields.join(',')
|
|
@@ -5,5 +5,9 @@
|
|
|
5
5
|
import { FEATURE_NAMES } from '../../loaders/features/features'
|
|
6
6
|
|
|
7
7
|
export const FEATURE_NAME = FEATURE_NAMES.ajax
|
|
8
|
-
|
|
8
|
+
export const CAPTURE_PAYLOAD_SETTINGS = {
|
|
9
|
+
NONE: 'none',
|
|
10
|
+
FAILURES: 'failures',
|
|
11
|
+
ALL: 'all'
|
|
12
|
+
}
|
|
9
13
|
export const AJAX_ID = 'ajaxRequest.id'
|