@newrelic/browser-agent 1.316.0-rc.6 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/common/config/init-types.js +1 -0
  3. package/dist/cjs/common/config/init.js +6 -4
  4. package/dist/cjs/common/constants/env.cdn.js +1 -1
  5. package/dist/cjs/common/constants/env.npm.js +1 -1
  6. package/dist/cjs/common/payloads/payloads.js +127 -0
  7. package/dist/cjs/common/serialize/bel-serializer.js +2 -1
  8. package/dist/cjs/common/util/data-size.js +2 -2
  9. package/dist/cjs/common/wrap/wrap-xhr.js +1 -1
  10. package/dist/cjs/features/ajax/aggregate/gql.js +40 -1
  11. package/dist/cjs/features/ajax/aggregate/index.js +46 -13
  12. package/dist/cjs/features/ajax/constants.js +6 -1
  13. package/dist/cjs/features/ajax/instrument/index.js +105 -17
  14. package/dist/cjs/features/jserrors/aggregate/format-stack-trace.js +2 -2
  15. package/dist/cjs/features/jserrors/aggregate/index.js +1 -1
  16. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +35 -4
  17. package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  18. package/dist/esm/common/config/init-types.js +1 -0
  19. package/dist/esm/common/config/init.js +3 -1
  20. package/dist/esm/common/constants/env.cdn.js +1 -1
  21. package/dist/esm/common/constants/env.npm.js +1 -1
  22. package/dist/esm/common/payloads/payloads.js +118 -0
  23. package/dist/esm/common/serialize/bel-serializer.js +2 -1
  24. package/dist/esm/common/util/data-size.js +2 -2
  25. package/dist/esm/common/wrap/wrap-xhr.js +1 -1
  26. package/dist/esm/features/ajax/aggregate/gql.js +39 -1
  27. package/dist/esm/features/ajax/aggregate/index.js +47 -14
  28. package/dist/esm/features/ajax/constants.js +5 -0
  29. package/dist/esm/features/ajax/instrument/index.js +106 -18
  30. package/dist/esm/features/jserrors/aggregate/format-stack-trace.js +2 -2
  31. package/dist/esm/features/jserrors/aggregate/index.js +1 -1
  32. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +35 -4
  33. package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/dist/types/common/config/init-types.d.ts +4 -0
  36. package/dist/types/common/config/init-types.d.ts.map +1 -1
  37. package/dist/types/common/config/init.d.ts.map +1 -1
  38. package/dist/types/common/payloads/payloads.d.ts +42 -0
  39. package/dist/types/common/payloads/payloads.d.ts.map +1 -0
  40. package/dist/types/common/serialize/bel-serializer.d.ts +1 -1
  41. package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
  42. package/dist/types/features/ajax/aggregate/gql.d.ts +8 -0
  43. package/dist/types/features/ajax/aggregate/gql.d.ts.map +1 -1
  44. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  45. package/dist/types/features/ajax/constants.d.ts +5 -0
  46. package/dist/types/features/ajax/constants.d.ts.map +1 -1
  47. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  48. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +5 -0
  49. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  50. package/package.json +2 -2
  51. package/src/common/config/init-types.js +1 -0
  52. package/src/common/config/init.js +2 -1
  53. package/src/common/payloads/payloads.js +135 -0
  54. package/src/common/serialize/bel-serializer.js +2 -1
  55. package/src/common/util/data-size.js +2 -2
  56. package/src/common/wrap/wrap-xhr.js +1 -1
  57. package/src/features/ajax/aggregate/gql.js +42 -1
  58. package/src/features/ajax/aggregate/index.js +38 -13
  59. package/src/features/ajax/constants.js +5 -1
  60. package/src/features/ajax/instrument/index.js +114 -18
  61. package/src/features/jserrors/aggregate/format-stack-trace.js +3 -3
  62. package/src/features/jserrors/aggregate/index.js +1 -1
  63. package/src/features/soft_navigations/aggregate/ajax-node.js +25 -4
  64. package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
@@ -6,12 +6,13 @@ 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 { AJAX_ID, FEATURE_NAME } from '../constants'
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'
15
16
  import { Obfuscator } from '../../../common/util/obfuscate'
16
17
  import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils'
17
18
  import { EVENT_TYPES } from '../../../common/constants/events'
@@ -101,6 +102,24 @@ export class Aggregate extends AggregateBase {
101
102
  [AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
102
103
  }
103
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
+
104
123
  if (ctx.dt) {
105
124
  event.spanId = ctx.dt.spanId
106
125
  event.traceId = ctx.dt.traceId
@@ -109,11 +128,6 @@ export class Aggregate extends AggregateBase {
109
128
  )
110
129
  }
111
130
 
112
- // parsed from the AJAX body, looking for operationName param & parsing query for operationType
113
- event.gql = params.gql = parseGQL({
114
- body: ctx.body,
115
- query: ctx.parsedOrigin?.search
116
- })
117
131
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length)
118
132
 
119
133
  /** make a copy of the event for the MFE target if it exists */
@@ -136,7 +150,9 @@ export class Aggregate extends AggregateBase {
136
150
 
137
151
  serializer (eventBuffer) {
138
152
  if (!eventBuffer.length) return
139
- const addString = getAddStringContext(this.obfuscator)
153
+
154
+ const { addString, addStringWithTruncation } = createStringAdders(getAddStringContext, this.obfuscator)
155
+
140
156
  let payload = 'bel.7;'
141
157
 
142
158
  let firstTimestamp = 0
@@ -170,15 +186,24 @@ export class Aggregate extends AggregateBase {
170
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.
171
187
  const jsAttributes = this.agentRef.info.jsAttributes
172
188
 
173
- // add custom attributes
174
- // gql decorators are added as custom attributes to alleviate need for new BEL schema
175
- const attrParts = addCustomAttributes({
189
+ // Regular attributes: obfuscate only
190
+ const regularAttrs = addCustomAttributes({
176
191
  ...(jsAttributes || {}),
177
- ...(event.gql || {}),
192
+ [AJAX_ID]: event[AJAX_ID], // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
178
193
  ...(event.targetAttributes || {}), // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
179
- [AJAX_ID]: event[AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
194
+ ...(event.gql || {})
180
195
  }, addString)
181
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]
182
207
  fields.unshift(numeric(attrParts.length))
183
208
 
184
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'
@@ -14,7 +14,7 @@ import { parseUrl } from '../../../common/url/parse-url'
14
14
  import { DT } from './distributed-tracing'
15
15
  import { responseSizeFromXhr } from './response-size'
16
16
  import { InstrumentBase } from '../../utils/instrument-base'
17
- import { FEATURE_NAME } from '../constants'
17
+ import { CAPTURE_PAYLOAD_SETTINGS, FEATURE_NAME } from '../constants'
18
18
  import { FEATURE_NAMES } from '../../../loaders/features/features'
19
19
  import { SUPPORTABILITY_METRIC } from '../../metrics/constants'
20
20
  import { now } from '../../../common/timing/now'
@@ -27,6 +27,7 @@ var handlersLen = handlers.length
27
27
  var origRequest = gosNREUMOriginals().o.REQ
28
28
  var origXHR = gosNREUMOriginals().o.XHR
29
29
  const NR_CAT_HEADER = 'X-NewRelic-App-Data'
30
+ const INTERNAL_ERROR = 'internal-error'
30
31
 
31
32
  export class Instrument extends InstrumentBase {
32
33
  static featureName = FEATURE_NAME
@@ -62,16 +63,18 @@ export class Instrument extends InstrumentBase {
62
63
  wrapFetch(this.ee, agentRef)
63
64
  wrapXhr(this.ee, agentRef)
64
65
  subscribeToEvents(agentRef, this.ee, this.handler, this.dt)
65
-
66
66
  this.importAggregator(agentRef, () => import(/* webpackChunkName: "ajax-aggregate" */ '../aggregate/index.js'))
67
67
  }
68
68
  }
69
69
 
70
70
  function subscribeToEvents (agentRef, ee, handler, dt) {
71
+ const shouldInterceptPayloads = [CAPTURE_PAYLOAD_SETTINGS.ALL, CAPTURE_PAYLOAD_SETTINGS.FAILURES].includes(agentRef.init.ajax?.capture_payloads)
72
+
71
73
  ee.on('new-xhr', onNewXhr)
72
74
  ee.on('open-xhr-start', onOpenXhrStart)
73
75
  ee.on('open-xhr-end', onOpenXhrEnd)
74
76
  ee.on('send-xhr-start', onSendXhrStart)
77
+ ee.on('setRequestHeader-xhr-start', onSetRequestHeader)
75
78
  ee.on('xhr-cb-time', onXhrCbTime)
76
79
  ee.on('xhr-load-added', onXhrLoadAdded)
77
80
  ee.on('xhr-load-removed', onXhrLoadRemoved)
@@ -147,6 +150,14 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
147
150
  }
148
151
  }
149
152
 
153
+ function onSetRequestHeader (args, xhr) {
154
+ // args[0] = header name, args[1] = header value
155
+ if (shouldInterceptPayloads && args.length >= 2) {
156
+ this.requestHeaders ??= {}
157
+ this.requestHeaders[args[0].toLowerCase()] = args[1]
158
+ }
159
+ }
160
+
150
161
  function onSendXhrStart (args, xhr) {
151
162
  var metrics = this.metrics
152
163
  var data = args[0]
@@ -159,7 +170,7 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
159
170
 
160
171
  this.startTime = now()
161
172
 
162
- this.body = data
173
+ this.requestBody = data
163
174
 
164
175
  this.listener = function (evt) {
165
176
  try {
@@ -169,7 +180,7 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
169
180
  if (evt.type !== 'load' || ((context.called === context.totalCbs) && (context.onloadCalled || typeof (xhr.onload) !== 'function') && typeof context.end === 'function')) context.end(xhr)
170
181
  } catch (e) {
171
182
  try {
172
- ee.emit('internal-error', [e])
183
+ ee.emit(INTERNAL_ERROR, [e])
173
184
  } catch (err) {
174
185
  // do nothing
175
186
  }
@@ -321,9 +332,29 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
321
332
  const method = ('' + ((target && target instanceof origRequest && target.method) ||
322
333
  opts.method || 'GET')).toUpperCase()
323
334
  this.params.method = method
324
- this.body = opts.body
325
335
 
326
- this.txSize = dataSize(opts.body) || 0
336
+ this.txSize = dataSize(opts.body || (target?.body)) || 0
337
+
338
+ // Capture request headers
339
+ try {
340
+ var headers = opts.headers || (target?.headers)
341
+ if (shouldInterceptPayloads && headers) {
342
+ this.requestHeaders ??= {}
343
+ if (headers instanceof Headers) {
344
+ headers.forEach(function (value, key) {
345
+ this.requestHeaders[key.toLowerCase()] = value
346
+ }.bind(this))
347
+ } else if (typeof headers === 'object') {
348
+ for (var key in headers) {
349
+ this.requestHeaders[key.toLowerCase()] = headers[key]
350
+ }
351
+ }
352
+ }
353
+ } catch (e) {
354
+ // Silently fail if we can't access headers
355
+ }
356
+
357
+ this.requestBody = opts.body || (target?.body)
327
358
  }
328
359
 
329
360
  // we capture failed call as status 0, the actual error is ignored
@@ -335,20 +366,47 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
335
366
 
336
367
  this.params.status = res ? res.status : 0
337
368
 
338
- // convert rxSize to a number
339
- let responseSize
340
- if (typeof this.rxSize === 'string' && this.rxSize.length > 0) {
341
- responseSize = +this.rxSize
369
+ const finishAndReport = () => {
370
+ // convert rxSize to a number - handle both string (from content-length header) and number (from fallback)
371
+ const num = +this.rxSize
372
+ const responseSize = this.rxSize != null && !isNaN(num) ? num : undefined
373
+
374
+ const metrics = {
375
+ txSize: this.txSize,
376
+ rxSize: responseSize,
377
+ duration: this.endTime - this.startTime
378
+ }
379
+
380
+ const payload = [this.params, metrics, this.startTime, this.endTime, 'fetch']
381
+ this.targets.forEach(target => reportToAgg(payload, this, target))
342
382
  }
343
383
 
344
- const metrics = {
345
- txSize: this.txSize,
346
- rxSize: responseSize,
347
- duration: now() - this.startTime
384
+ /** Since accessing fetch bodies is an async process, these are
385
+ * reasonable conditions to check to not do needless extra work */
386
+ if (!res || !shouldInterceptPayloads) {
387
+ finishAndReport()
388
+ return
348
389
  }
349
390
 
350
- const payload = [this.params, metrics, this.startTime, this.endTime, 'fetch']
351
- this.targets.forEach(target => reportToAgg(payload, this, target))
391
+ // Clone the response to read the body without consuming the original
392
+ res.clone().text().then((text) => {
393
+ this.responseBody = text
394
+ // Use captured payload size as fallback if content-length header was missing or is 0 with a body
395
+ // Only apply fallback for non-network-error responses (status !== 0)
396
+ if ((!this.rxSize || this.rxSize === '0' || this.rxSize === 0) && text !== undefined && this.params.status !== 0) {
397
+ this.rxSize = dataSize(text)
398
+ }
399
+ if (res?.headers) {
400
+ this.responseHeaders = {}
401
+ res.headers.forEach(function (value, key) {
402
+ this.responseHeaders[key.toLowerCase()] = value
403
+ }.bind(this))
404
+ }
405
+ }).catch((err) => {
406
+ ee.emit(INTERNAL_ERROR, [err])
407
+ }).finally(() => {
408
+ finishAndReport()
409
+ })
352
410
  }
353
411
 
354
412
  // Create report for XHR request that has finished
@@ -365,7 +423,7 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
365
423
  if (params.aborted) return
366
424
  if (hasUndefinedHostname(params)) return // don't bother with XHR of url with no hostname
367
425
 
368
- metrics.duration = now() - this.startTime
426
+ metrics.duration = this.endTime - this.startTime
369
427
  if (!this.loadCaptureCalled && xhr.readyState === 4) {
370
428
  captureXhrData(this, xhr)
371
429
  } else if (params.status == null) {
@@ -375,6 +433,27 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
375
433
  // Always send cbTime, even if no noticeable time was taken.
376
434
  metrics.cbTime = this.cbTime
377
435
 
436
+ if (shouldInterceptPayloads) {
437
+ try {
438
+ this.responseBody = xhr.responseText
439
+ } catch (e) {
440
+ this.responseBody = xhr.response
441
+ }
442
+
443
+ // Use captured payload size as fallback if not already determined or is 0 with a body
444
+ // Only apply fallback for non-network-error responses (status !== 0)
445
+ if ((!metrics.rxSize || metrics.rxSize === 0) && this.responseBody !== undefined && params.status !== 0) {
446
+ const size = dataSize(this.responseBody)
447
+ if (size !== undefined) metrics.rxSize = size
448
+ }
449
+
450
+ try {
451
+ this.responseHeaders = parseResponseHeaders(xhr.getAllResponseHeaders())
452
+ } catch (err) {
453
+ ee.emit(INTERNAL_ERROR, [err])
454
+ }
455
+ }
456
+
378
457
  const payload = [params, metrics, this.startTime, this.endTime, 'xhr']
379
458
  this.targets.forEach(target => reportToAgg(payload, this, target))
380
459
  }
@@ -387,7 +466,8 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
387
466
  ctx.params.status = xhr.status
388
467
 
389
468
  var size = responseSizeFromXhr(xhr, ctx.lastSize)
390
- if (size) ctx.metrics.rxSize = size
469
+ // Don't set rxSize for network errors (status 0)
470
+ if (size !== undefined && xhr.status !== 0) ctx.metrics.rxSize = size
391
471
 
392
472
  if (ctx.sameOrigin && xhr.getAllResponseHeaders().indexOf(NR_CAT_HEADER) >= 0) {
393
473
  var header = xhr.getResponseHeader(NR_CAT_HEADER)
@@ -414,4 +494,20 @@ function addUrl (ctx, url) {
414
494
  ctx.sameOrigin = parsed.sameOrigin
415
495
  }
416
496
 
497
+ function parseResponseHeaders (headerStr) {
498
+ const headers = {}
499
+ if (!headerStr) return headers
500
+
501
+ headerStr.split('\r\n').forEach(function (line) {
502
+ const separatorIndex = line.indexOf(': ')
503
+ if (separatorIndex > 0) {
504
+ const name = line.substring(0, separatorIndex)
505
+ const value = line.substring(separatorIndex + 2)
506
+ headers[name.toLowerCase()] = value
507
+ }
508
+ })
509
+
510
+ return headers
511
+ }
512
+
417
513
  export const Ajax = Instrument
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
@@ -10,7 +10,7 @@ export function formatStackTrace (stackLines) {
10
10
  return truncateStackLines(stackLines).replace(stripNewlinesRegex, '')
11
11
  }
12
12
 
13
- // takes array of stack lines and returns string with top 50 and buttom 50 lines
13
+ // takes array of stack lines and returns string with top 50 and bottom 50 lines
14
14
  function truncateStackLines (stackLines) {
15
15
  var stackString
16
16
  if (stackLines.length > 100) {
@@ -26,5 +26,5 @@ function truncateStackLines (stackLines) {
26
26
 
27
27
  // truncates stack string to limit what is sent to backend
28
28
  export function truncateSize (stackString) {
29
- return (stackString.length > MAX_STACK_TRACE_LENGTH) ? stackString.substr(0, MAX_STACK_TRACE_LENGTH) : stackString
29
+ return stackString.length > MAX_STACK_TRACE_LENGTH ? stackString.substr(0, MAX_STACK_TRACE_LENGTH) : stackString
30
30
  }
@@ -267,7 +267,7 @@ export class Aggregate extends AggregateBase {
267
267
  params.stack_trace = truncateSize(stackInfo.stackString)
268
268
  this.observedAt[bucketHash] = Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(time))
269
269
  } else {
270
- params.browser_stack_hash = stringHashCode(stackInfo.stackString)
270
+ params.browser_stack_hash = stringHashCode(truncateSize(stackInfo.stackString))
271
271
  }
272
272
  params.releaseIds = stringify(this.agentRef.runtime.releaseIds)
273
273
 
@@ -3,6 +3,7 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer'
6
+ import { createStringAdders } from '../../../common/payloads/payloads'
6
7
  import { AJAX_ID } from '../../ajax/constants'
7
8
  import { NODE_TYPE } from '../constants'
8
9
  import { BelNode } from './bel-node'
@@ -25,6 +26,13 @@ export class AjaxNode extends BelNode {
25
26
  this.targetAttributes = ajaxEvent.targetAttributes
26
27
  this[AJAX_ID] = ajaxEvent[AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
27
28
 
29
+ // optional payload metadata attributes
30
+ this.requestBody = ajaxEvent.requestBody
31
+ this.requestHeaders = ajaxEvent.requestHeaders
32
+ this.requestQuery = ajaxEvent.requestQuery
33
+ this.responseBody = ajaxEvent.responseBody
34
+ this.responseHeaders = ajaxEvent.responseHeaders
35
+
28
36
  this.start = ajaxEvent.startTime
29
37
  this.end = ajaxEvent.endTime
30
38
  if (ajaxContext?.latestLongtaskEnd) {
@@ -34,7 +42,8 @@ export class AjaxNode extends BelNode {
34
42
  }
35
43
 
36
44
  serialize (parentStartTimestamp, agentRef, ajaxObfuscator) {
37
- const addString = getAddStringContext(ajaxObfuscator)
45
+ const { addString, addStringWithTruncation } = createStringAdders(getAddStringContext, ajaxObfuscator)
46
+
38
47
  const nodeList = []
39
48
 
40
49
  // IMPORTANT: The order in which addString is called matters and correlates to the order in which string shows up in the harvest payload. Do not re-order the following code.
@@ -55,11 +64,23 @@ export class AjaxNode extends BelNode {
55
64
  addString(this.nodeId),
56
65
  nullable(this.spanId, addString, true) + nullable(this.traceId, addString, true) + nullable(this.spanTimestamp, numeric)
57
66
  ]
58
- let allAttachedNodes = addCustomAttributes({
59
- ...(this.gql || {}),
67
+ // Regular attributes: obfuscate only
68
+ const regularAttrs = addCustomAttributes({
69
+ [AJAX_ID]: this[AJAX_ID],
60
70
  ...(this.targetAttributes || {}),
61
- [AJAX_ID]: this[AJAX_ID]
71
+ ...(this.gql || {})
62
72
  }, addString)
73
+
74
+ // Payload attributes: obfuscate then truncate
75
+ const payloadAttrs = addCustomAttributes({
76
+ ...(this.requestBody ? { requestBody: this.requestBody } : {}),
77
+ ...(this.requestHeaders ? { requestHeaders: this.requestHeaders } : {}),
78
+ ...(this.requestQuery ? { requestQuery: this.requestQuery } : {}),
79
+ ...(this.responseBody ? { responseBody: this.responseBody } : {}),
80
+ ...(this.responseHeaders ? { responseHeaders: this.responseHeaders } : {})
81
+ }, addStringWithTruncation)
82
+
83
+ let allAttachedNodes = [...regularAttrs, ...payloadAttrs]
63
84
  this.children.forEach(node => allAttachedNodes.push(node.serialize())) // no children is expected under ajax nodes at this time
64
85
 
65
86
  fields[1] = numeric(allAttachedNodes.length)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { navTimingValues } from '../../../common/timing/nav-timing'