Skip to content

Commit ee1182c

Browse files
committed
Merge branch 'main' into NR-509779-improve-startup
2 parents 49da74d + c97aaf9 commit ee1182c

25 files changed

Lines changed: 1145 additions & 61 deletions

File tree

src/common/constants/events.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export const EVENT_TYPES = {
7+
AJAX: 'AjaxRequest',
8+
PA: 'PageAction',
9+
UA: 'UserAction',
10+
BP: 'BrowserPerformance',
11+
WS: 'WebSocket',
12+
SPV: 'SecurityPolicyViolation',
13+
JSE: 'JavaScriptError',
14+
LOG: 'Log',
15+
PVE: 'PageView',
16+
PVT: 'PageViewTiming',
17+
SR: 'SessionReplay',
18+
ST: 'SessionTrace',
19+
BI: 'BrowserInteraction'
20+
}

src/common/harvest/harvester.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { handle } from '../event-emitter/handle'
88
import { subscribeToEOL } from '../unload/eol'
99
import { getSubmitMethod, xhr as xhrMethod } from '../util/submit-data'
1010
import { send } from './send'
11+
import { Obfuscator } from '../util/obfuscate'
1112

1213
const RETRY = 'Harvester/Retry/'
1314
const RETRY_ATTEMPTED = RETRY + 'Attempted/'
@@ -21,6 +22,10 @@ export class Harvester {
2122
constructor (agentRef) {
2223
this.agentRef = agentRef
2324

25+
// Create obfuscator for harvest metadata (referrer URL, etc.)
26+
// No event type specified - applies to all harvests regardless of feature
27+
this.obfuscator = new Obfuscator(agentRef)
28+
2429
subscribeToEOL(() => { // do one last harvest round or check
2530
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
2631
if (typeof aggregateInst.harvestOpts.beforeUnload === 'function') aggregateInst.harvestOpts.beforeUnload()
@@ -65,6 +70,7 @@ export class Harvester {
6570
payload: output.payload,
6671
localOpts,
6772
submitMethod,
73+
harvesterObfuscator: this.obfuscator,
6874
cbFinished,
6975
raw: aggregateInst.harvestOpts.raw,
7076
featureName: aggregateInst.featureName,

src/common/harvest/send.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const warnings = {}
2727
* @param {NetworkSendSpec} spec Specification for sending data
2828
* @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
2929
*/
30-
export function send (agentRef, { endpoint, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName, endpointVersion = 1 }) {
30+
export function send (agentRef, { endpoint, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName, endpointVersion = 1, harvesterObfuscator }) {
3131
if (!agentRef.info.errorBeacon) return false
3232

3333
let { body, qs } = cleanPayload(payload)
@@ -42,7 +42,7 @@ export function send (agentRef, { endpoint, payload, localOpts = {}, submitMetho
4242
const url = raw
4343
? `${protocol}://${perceivedBeacon}/${endpoint}`
4444
: `${protocol}://${perceivedBeacon}${endpoint !== RUM ? '/' + endpoint : ''}/${endpointVersion}/${agentRef.info.licenseKey}`
45-
const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint) : ''
45+
const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) : ''
4646
let payloadParams = obj(qs, agentRef.runtime.maxBytes)
4747
if (baseParams === '' && payloadParams.startsWith('&')) {
4848
payloadParams = payloadParams.substring(1)
@@ -175,8 +175,9 @@ function cleanPayload (payload = {}) {
175175
}
176176

177177
// The stuff that gets sent every time.
178-
function baseQueryString (agentRef, qs, endpoint) {
179-
const ref = agentRef.runtime.obfuscator.obfuscateString(cleanURL('' + globalScope.location))
178+
function baseQueryString (agentRef, qs, endpoint, harvesterObfuscator) {
179+
const cleanedURL = cleanURL('' + globalScope.location)
180+
const ref = harvesterObfuscator?.obfuscateString(cleanedURL) ?? cleanedURL
180181
const session = agentRef.runtime.session
181182
const hr = !!session?.state.sessionReplaySentFirstChunk && session?.state.sessionReplayMode === 1 && endpoint !== JSERRORS
182183
const ht = !!session?.state.traceHarvestStarted && session?.state.sessionTraceMode === 1 && ![LOGS, BLOBS].includes(endpoint)

src/common/serialize/bel-serializer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
2+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -29,7 +29,7 @@ export function getAddStringContext (obfuscator) {
2929

3030
function addString (str) {
3131
if (typeof str === 'undefined' || str === '') return ''
32-
str = obfuscator.obfuscateString(String(str))
32+
str = obfuscator?.obfuscateString(String(str)) ?? String(str)
3333
if (hasOwnProp.call(stringTable, str)) {
3434
return numeric(stringTable[str], true)
3535
} else {

src/common/util/obfuscate.js

Lines changed: 154 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
2+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55
import { isFileProtocol } from '../url/protocol'
@@ -10,6 +10,7 @@ import { warn } from './console'
1010
* @typedef {object} ObfuscationRule
1111
* @property {string|RegExp} regex The regular expression to match against in the payload
1212
* @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.
1314
*/
1415

1516
/**
@@ -24,15 +25,34 @@ import { warn } from './console'
2425
*/
2526

2627
export class Obfuscator {
27-
constructor (agentRef) {
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) {
2834
this.agentRef = agentRef
35+
this.eventType = eventType
2936
this.warnedRegexMissing = false
3037
this.warnedInvalidRegex = false
3138
this.warnedInvalidReplacement = false
3239
}
3340

3441
get obfuscateConfigRules () {
35-
return this.agentRef.init.obfuscate || []
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+
})
3656
}
3757

3858
/**
@@ -60,6 +80,137 @@ export class Obfuscator {
6080
}, input)
6181
}
6282

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+
63214
/**
64215
* Validates an obfuscation rule and provides errors if any are found.
65216
* @param {ObfuscationRule} rule The rule to validate

src/common/wrap/wrap-websocket.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { now } from '../timing/now'
88
import { cleanURL } from '../url/clean-url'
99
import { gosNREUMOriginals } from '../window/nreum'
1010
import { subscribeToPageUnload } from '../window/page-visibility'
11+
import { EVENT_TYPES } from '../constants/events'
1112

1213
const wrapped = {}
1314
const openWebSockets = new Set() // track all instances to close out metrics on page unload
@@ -37,7 +38,7 @@ export function wrapWebSocket (sharedEE) {
3738
})
3839

3940
class WrappedWebSocket extends WebSocket {
40-
static name = 'WebSocket'
41+
static name = EVENT_TYPES.WS
4142
static toString () { // fake native WebSocket when static class is stringified
4243
return 'function WebSocket() { [native code] }'
4344
}

src/features/ajax/aggregate/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import { AggregateBase } from '../../utils/aggregate-base'
1212
import { parseGQL } from './gql'
1313
import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
1414
import { gosNREUMOriginals } from '../../../common/window/nreum'
15+
import { Obfuscator } from '../../../common/util/obfuscate'
1516
import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils'
17+
import { EVENT_TYPES } from '../../../common/constants/events'
1618
import { generateUuid } from '../../../common/ids/unique-id'
1719

1820
export class Aggregate extends AggregateBase {
@@ -23,6 +25,9 @@ export class Aggregate extends AggregateBase {
2325
setDenyList(agentRef.runtime.denyList)
2426
const classThis = this
2527

28+
// Create obfuscator for AJAX requests
29+
this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.AJAX)
30+
2631
if (!agentRef.init.ajax.block_internal) {
2732
// 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.
2833
super.canHarvestEarly = false
@@ -131,7 +136,7 @@ export class Aggregate extends AggregateBase {
131136

132137
serializer (eventBuffer) {
133138
if (!eventBuffer.length) return
134-
const addString = getAddStringContext(this.agentRef.runtime.obfuscator)
139+
const addString = getAddStringContext(this.obfuscator)
135140
let payload = 'bel.7;'
136141

137142
let firstTimestamp = 0

0 commit comments

Comments
 (0)