@newrelic/browser-agent 1.298.0 → 1.299.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 (80) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +12 -2
  3. package/dist/cjs/common/constants/env.cdn.js +1 -1
  4. package/dist/cjs/common/constants/env.npm.js +1 -1
  5. package/dist/cjs/common/dom/selector-path.js +60 -44
  6. package/dist/cjs/common/harvest/harvester.js +37 -7
  7. package/dist/cjs/common/url/extract-url.js +21 -0
  8. package/dist/cjs/features/ajax/instrument/index.js +2 -9
  9. package/dist/cjs/features/generic_events/aggregate/index.js +29 -8
  10. package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
  11. package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +71 -33
  12. package/dist/cjs/features/generic_events/constants.js +2 -1
  13. package/dist/cjs/features/generic_events/instrument/index.js +45 -0
  14. package/dist/cjs/features/metrics/aggregate/framework-detection.js +2 -0
  15. package/dist/cjs/features/metrics/aggregate/harvest-metadata.js +45 -0
  16. package/dist/cjs/features/metrics/aggregate/index.js +21 -0
  17. package/dist/cjs/features/session_replay/aggregate/index.js +1 -1
  18. package/dist/cjs/features/session_replay/shared/recorder.js +6 -2
  19. package/dist/cjs/loaders/api/noticeError.js +1 -0
  20. package/dist/cjs/loaders/configure/configure.js +1 -0
  21. package/dist/esm/common/constants/env.cdn.js +1 -1
  22. package/dist/esm/common/constants/env.npm.js +1 -1
  23. package/dist/esm/common/dom/selector-path.js +58 -42
  24. package/dist/esm/common/harvest/harvester.js +37 -7
  25. package/dist/esm/common/url/extract-url.js +15 -0
  26. package/dist/esm/features/ajax/instrument/index.js +2 -9
  27. package/dist/esm/features/generic_events/aggregate/index.js +29 -8
  28. package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
  29. package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +72 -34
  30. package/dist/esm/features/generic_events/constants.js +1 -0
  31. package/dist/esm/features/generic_events/instrument/index.js +46 -1
  32. package/dist/esm/features/metrics/aggregate/framework-detection.js +2 -0
  33. package/dist/esm/features/metrics/aggregate/harvest-metadata.js +39 -0
  34. package/dist/esm/features/metrics/aggregate/index.js +21 -0
  35. package/dist/esm/features/session_replay/aggregate/index.js +1 -1
  36. package/dist/esm/features/session_replay/shared/recorder.js +6 -2
  37. package/dist/esm/loaders/api/noticeError.js +1 -0
  38. package/dist/esm/loaders/configure/configure.js +1 -0
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/dist/types/common/dom/selector-path.d.ts +6 -1
  41. package/dist/types/common/dom/selector-path.d.ts.map +1 -1
  42. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  43. package/dist/types/common/url/extract-url.d.ts +7 -0
  44. package/dist/types/common/url/extract-url.d.ts.map +1 -0
  45. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  46. package/dist/types/features/generic_events/aggregate/index.d.ts +1 -3
  47. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  48. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts +3 -1
  49. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts.map +1 -1
  50. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts +3 -0
  51. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -1
  52. package/dist/types/features/generic_events/constants.d.ts +1 -0
  53. package/dist/types/features/generic_events/constants.d.ts.map +1 -1
  54. package/dist/types/features/generic_events/instrument/index.d.ts +2 -0
  55. package/dist/types/features/generic_events/instrument/index.d.ts.map +1 -1
  56. package/dist/types/features/metrics/aggregate/framework-detection.d.ts.map +1 -1
  57. package/dist/types/features/metrics/aggregate/harvest-metadata.d.ts +6 -0
  58. package/dist/types/features/metrics/aggregate/harvest-metadata.d.ts.map +1 -0
  59. package/dist/types/features/metrics/aggregate/index.d.ts +2 -0
  60. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  61. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  62. package/dist/types/loaders/api/noticeError.d.ts.map +1 -1
  63. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/common/dom/selector-path.js +51 -39
  66. package/src/common/harvest/harvester.js +34 -7
  67. package/src/common/url/extract-url.js +17 -0
  68. package/src/features/ajax/instrument/index.js +2 -10
  69. package/src/features/generic_events/aggregate/index.js +23 -8
  70. package/src/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
  71. package/src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +80 -24
  72. package/src/features/generic_events/constants.js +2 -0
  73. package/src/features/generic_events/instrument/index.js +51 -1
  74. package/src/features/metrics/aggregate/framework-detection.js +2 -0
  75. package/src/features/metrics/aggregate/harvest-metadata.js +42 -0
  76. package/src/features/metrics/aggregate/index.js +21 -0
  77. package/src/features/session_replay/aggregate/index.js +1 -1
  78. package/src/features/session_replay/shared/recorder.js +5 -1
  79. package/src/loaders/api/noticeError.js +1 -0
  80. package/src/loaders/configure/configure.js +1 -0
@@ -135,14 +135,16 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
135
135
 
136
136
  const fullUrl = `${url}?${baseParams}${payloadParams}`
137
137
  const gzip = !!qs?.attributes?.includes('gzip')
138
- if (!gzip) {
139
- if (endpoint !== EVENTS) body = stringify(body) // all features going to 'events' endpoint should already be serialized & stringified
140
- // Warn--once per endpoint--if the agent tries to send large payloads
141
- if (body.length > 750000 && (warnings[endpoint] = (warnings[endpoint] || 0) + 1) === 1) warn(28, endpoint)
142
- }
138
+
139
+ // all gzipped data is already in the correct format and needs no transformation
140
+ // all features going to 'events' endpoint should already be serialized & stringified
141
+ let stringBody = gzip || endpoint === EVENTS ? body : stringify(body)
143
142
 
144
143
  // If body is null, undefined, or an empty object or array after stringifying, send an empty string instead.
145
- if (!body || body.length === 0 || body === '{}' || body === '[]') body = ''
144
+ if (!stringBody || stringBody.length === 0 || stringBody === '{}' || stringBody === '[]') stringBody = ''
145
+
146
+ // Warn--once per endpoint--if the agent tries to send large payloads
147
+ if (endpoint !== BLOBS && stringBody.length > 750000 && (warnings[endpoint] = (warnings[endpoint] || 0) + 1) === 1) warn(28, endpoint)
146
148
 
147
149
  const headers = [{ key: 'content-type', value: 'text/plain' }]
148
150
 
@@ -150,7 +152,7 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
150
152
  Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
151
153
  we just make a BLOCKING request--trivial impact--with the remaining data as a temp fill-in for sendBeacon.
152
154
  Following the removal of img-element method. */
153
- let result = submitMethod({ url: fullUrl, body, sync: localOpts.isFinalHarvest && isWorkerScope, headers })
155
+ let result = submitMethod({ url: fullUrl, body: stringBody, sync: localOpts.isFinalHarvest && isWorkerScope, headers })
154
156
 
155
157
  if (!localOpts.isFinalHarvest && cbFinished) { // final harvests don't hold onto buffer data (shouldRetryOnFail is false), so cleanup isn't needed
156
158
  if (submitMethod === xhrMethod) {
@@ -160,6 +162,9 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
160
162
  const cbResult = { sent: this.status !== 0, status: this.status, retry: shouldRetry(this.status), fullUrl, xhr: this, targetApp }
161
163
  if (localOpts.needResponse) cbResult.responseText = this.responseText
162
164
  cbFinished(cbResult)
165
+
166
+ /** temporary audit of consistency of harvest metadata flags */
167
+ if (!shouldRetry(this.status)) trackHarvestMetadata()
163
168
  }, eventListenerOpts(false))
164
169
  } else if (submitMethod === fetchMethod) {
165
170
  result.then(async function (response) {
@@ -167,8 +172,30 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
167
172
  const cbResult = { sent: true, status, retry: shouldRetry(status), fullUrl, fetchResponse: response, targetApp }
168
173
  if (localOpts.needResponse) cbResult.responseText = await response.text()
169
174
  cbFinished(cbResult)
175
+ /** temporary audit of consistency of harvest metadata flags */
176
+ if (!shouldRetry(status)) trackHarvestMetadata()
170
177
  })
171
178
  }
179
+
180
+ function trackHarvestMetadata () {
181
+ try {
182
+ if (featureName === FEATURE_NAMES.jserrors && !body?.err) return
183
+
184
+ const hasReplay = baseParams.includes('hr=1')
185
+ const hasTrace = baseParams.includes('ht=1')
186
+ const hasError = qs?.attributes?.includes('hasError=true')
187
+
188
+ handle('harvest-metadata', [{
189
+ [featureName]: {
190
+ ...(hasReplay && { hasReplay }),
191
+ ...(hasTrace && { hasTrace }),
192
+ ...(hasError && { hasError })
193
+ }
194
+ }], undefined, FEATURE_NAMES.metrics, agentRef.ee)
195
+ } catch (err) {
196
+ // do nothing
197
+ }
198
+ }
172
199
  }
173
200
 
174
201
  dispatchGlobalEvent({
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Copyright 2020-2025 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { globalScope } from '../constants/runtime'
6
+ import { gosNREUMOriginals } from '../window/nreum'
7
+
8
+ /**
9
+ * Extracts a URL from various target types.
10
+ * @param {string|Request|URL} target - The target to extract the URL from. It can be a string, a Fetch Request object, or a URL object.
11
+ * @returns {string|undefined} The extracted URL as a string, or undefined if the target type is not supported.
12
+ */
13
+ export function extractUrl (target) {
14
+ if (typeof target === 'string') return target
15
+ else if (target instanceof gosNREUMOriginals().o.REQ) return target.url
16
+ else if (globalScope?.URL && target instanceof URL) return target.href
17
+ }
@@ -19,6 +19,7 @@ import { FEATURE_NAMES } from '../../../loaders/features/features'
19
19
  import { SUPPORTABILITY_METRIC } from '../../metrics/constants'
20
20
  import { now } from '../../../common/timing/now'
21
21
  import { hasUndefinedHostname } from '../../../common/deny-list/deny-list'
22
+ import { extractUrl } from '../../../common/url/extract-url'
22
23
 
23
24
  var handlers = ['load', 'error', 'abort', 'timeout']
24
25
  var handlersLen = handlers.length
@@ -318,16 +319,7 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
318
319
 
319
320
  var opts = this.opts || {}
320
321
  var target = this.target
321
-
322
- var url
323
- if (typeof target === 'string') {
324
- url = target
325
- } else if (typeof target === 'object' && target instanceof origRequest) {
326
- url = target.url
327
- } else if (globalScope?.URL && typeof target === 'object' && target instanceof URL) {
328
- url = target.href
329
- }
330
- addUrl(this, url)
322
+ addUrl(this, extractUrl(target))
331
323
 
332
324
  var method = ('' + ((target && target instanceof origRequest && target.method) ||
333
325
  opts.method || 'GET')).toUpperCase()
@@ -17,6 +17,8 @@ import { isPureObject } from '../../../common/util/type-check'
17
17
 
18
18
  export class Aggregate extends AggregateBase {
19
19
  static featureName = FEATURE_NAME
20
+ #userActionAggregator
21
+
20
22
  constructor (agentRef) {
21
23
  super(agentRef, FEATURE_NAME)
22
24
  this.referrerUrl = (isBrowserScope && document.referrer) ? cleanURL(document.referrer) : undefined
@@ -28,7 +30,7 @@ export class Aggregate extends AggregateBase {
28
30
  return
29
31
  }
30
32
 
31
- this.trackSupportabilityMetrics()
33
+ this.#trackSupportabilityMetrics()
32
34
 
33
35
  registerHandler('api-recordCustomEvent', (timestamp, eventType, attributes) => {
34
36
  if (RESERVED_EVENT_TYPES.includes(eventType)) return warn(46)
@@ -59,8 +61,8 @@ export class Aggregate extends AggregateBase {
59
61
 
60
62
  let addUserAction = () => { /** no-op */ }
61
63
  if (isBrowserScope && agentRef.init.user_actions.enabled) {
62
- this.userActionAggregator = new UserActionsAggregator()
63
- this.harvestOpts.beforeUnload = () => addUserAction?.(this.userActionAggregator.aggregationEvent)
64
+ this.#userActionAggregator = new UserActionsAggregator(agentRef.init.feature_flags.includes('user_frustrations'))
65
+ this.harvestOpts.beforeUnload = () => addUserAction?.(this.#userActionAggregator.aggregationEvent)
64
66
 
65
67
  addUserAction = (aggregatedUserAction) => {
66
68
  try {
@@ -68,7 +70,7 @@ export class Aggregate extends AggregateBase {
68
70
  * so we still need to validate that an event was given to this method before we try to add */
69
71
  if (aggregatedUserAction?.event) {
70
72
  const { target, timeStamp, type } = aggregatedUserAction.event
71
- this.addEvent({
73
+ const userActionEvent = {
72
74
  eventType: 'UserAction',
73
75
  timestamp: this.toEpoch(timeStamp),
74
76
  action: type,
@@ -84,8 +86,12 @@ export class Aggregate extends AggregateBase {
84
86
  if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128)
85
87
  return acc
86
88
  }, {})),
87
- ...aggregatedUserAction.nearestTargetFields
88
- })
89
+ ...aggregatedUserAction.nearestTargetFields,
90
+ ...(aggregatedUserAction.deadClick && { deadClick: true }),
91
+ ...(aggregatedUserAction.errorClick && { errorClick: true })
92
+ }
93
+ this.addEvent(userActionEvent)
94
+ this.#trackUserActionSM(userActionEvent)
89
95
 
90
96
  /**
91
97
  * Returns the original target field name with `target` prepended and camelCased
@@ -116,8 +122,11 @@ export class Aggregate extends AggregateBase {
116
122
 
117
123
  registerHandler('ua', (evt) => {
118
124
  /** the processor will return the previously aggregated event if it has been completed by processing the current event */
119
- addUserAction(this.userActionAggregator.process(evt, this.agentRef.init.user_actions.elementAttributes))
125
+ addUserAction(this.#userActionAggregator.process(evt, this.agentRef.init.user_actions.elementAttributes))
120
126
  }, this.featureName, this.ee)
127
+ registerHandler('navChange', () => { this.#userActionAggregator.isLiveClick() }, this.featureName, this.ee)
128
+ registerHandler('uaXhr', () => { this.#userActionAggregator.isLiveClick() }, this.featureName, this.ee)
129
+ registerHandler('uaErr', () => this.#userActionAggregator.markAsErrorClick(), this.featureName, this.ee)
121
130
  }
122
131
 
123
132
  /**
@@ -296,7 +305,7 @@ export class Aggregate extends AggregateBase {
296
305
  return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp))
297
306
  }
298
307
 
299
- trackSupportabilityMetrics () {
308
+ #trackSupportabilityMetrics () {
300
309
  /** track usage SMs to improve these experimental features */
301
310
  const configPerfTag = 'Config/Performance/'
302
311
  if (this.agentRef.init.performance.capture_marks) this.reportSupportabilityMetric(configPerfTag + 'CaptureMarks/Enabled')
@@ -306,4 +315,10 @@ export class Aggregate extends AggregateBase {
306
315
  if (this.agentRef.init.performance.resources.first_party_domains?.length !== 0) this.reportSupportabilityMetric(configPerfTag + 'Resources/FirstPartyDomains/Changed')
307
316
  if (this.agentRef.init.performance.resources.ignore_newrelic === false) this.reportSupportabilityMetric(configPerfTag + 'Resources/IgnoreNewrelic/Changed')
308
317
  }
318
+
319
+ #trackUserActionSM (ua) {
320
+ if (ua.rageClick) this.reportSupportabilityMetric('UserAction/RageClick/Seen')
321
+ if (ua.deadClick) this.reportSupportabilityMetric('UserAction/DeadClick/Seen')
322
+ if (ua.errorClick) this.reportSupportabilityMetric('UserAction/ErrorClick/Seen')
323
+ }
309
324
  }
@@ -6,15 +6,17 @@ import { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../cons
6
6
  import { cleanURL } from '../../../../common/url/clean-url'
7
7
 
8
8
  export class AggregatedUserAction {
9
- constructor (evt, selectorPath, nearestTargetFields) {
9
+ constructor (evt, selectorInfo) {
10
10
  this.event = evt
11
11
  this.count = 1
12
12
  this.originMs = Math.floor(evt.timeStamp)
13
13
  this.relativeMs = [0]
14
- this.selectorPath = selectorPath
14
+ this.selectorPath = selectorInfo.path
15
15
  this.rageClick = undefined
16
- this.nearestTargetFields = nearestTargetFields
16
+ this.nearestTargetFields = selectorInfo.nearestFields
17
17
  this.currentUrl = cleanURL('' + location)
18
+ this.deadClick = false
19
+ this.errorClick = false
18
20
  }
19
21
 
20
22
  /**
@@ -2,14 +2,27 @@
2
2
  * Copyright 2020-2025 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import { generateSelectorPath } from '../../../../common/dom/selector-path'
6
- import { OBSERVED_WINDOW_EVENTS } from '../../constants'
5
+ import { analyzeElemPath } from '../../../../common/dom/selector-path'
6
+ import { FRUSTRATION_TIMEOUT_MS, OBSERVED_WINDOW_EVENTS } from '../../constants'
7
7
  import { AggregatedUserAction } from './aggregated-user-action'
8
+ import { Timer } from '../../../../common/timer/timer'
9
+ import { gosNREUMOriginals } from '../../../../common/window/nreum'
8
10
 
9
11
  export class UserActionsAggregator {
10
12
  /** @type {AggregatedUserAction=} */
11
13
  #aggregationEvent = undefined
12
14
  #aggregationKey = ''
15
+ #ufEnabled = false
16
+ #deadClickTimer = undefined
17
+ #domObserver = undefined
18
+ #errorClickTimer = undefined
19
+
20
+ constructor (userFrustrationsEnabled) {
21
+ if (userFrustrationsEnabled && gosNREUMOriginals().o.MO) {
22
+ this.#domObserver = new MutationObserver(this.isLiveClick.bind(this))
23
+ this.#ufEnabled = true
24
+ }
25
+ }
13
26
 
14
27
  get aggregationEvent () {
15
28
  // if this is accessed externally, we need to be done aggregating on it
@@ -28,40 +41,83 @@ export class UserActionsAggregator {
28
41
  */
29
42
  process (evt, targetFields) {
30
43
  if (!evt) return
31
- const { selectorPath, nearestTargetFields } = getSelectorPath(evt, targetFields)
32
- const aggregationKey = getAggregationKey(evt, selectorPath)
44
+ const targetElem = OBSERVED_WINDOW_EVENTS.includes(evt.type) ? window : evt.target
45
+ const selectorInfo = analyzeElemPath(targetElem, targetFields)
46
+
47
+ // if selectorInfo.path is undefined, aggregation will be skipped for this event
48
+ const aggregationKey = getAggregationKey(evt, selectorInfo.path)
33
49
  if (!!aggregationKey && aggregationKey === this.#aggregationKey) {
34
50
  // an aggregation exists already, so lets just continue to increment
35
51
  this.#aggregationEvent.aggregate(evt)
36
52
  } else {
37
53
  // return the prev existing one (if there is one)
38
54
  const finishedEvent = this.#aggregationEvent
39
- // then set as this new event aggregation
55
+ if (this.#ufEnabled) {
56
+ this.#deadClickCleanup()
57
+ this.#errorClickCleanup()
58
+ }
59
+
60
+ // then start new event aggregation
40
61
  this.#aggregationKey = aggregationKey
41
- this.#aggregationEvent = new AggregatedUserAction(evt, selectorPath, nearestTargetFields)
62
+ this.#aggregationEvent = new AggregatedUserAction(evt, selectorInfo)
63
+ if (this.#ufEnabled && evt.type === 'click' && (selectorInfo.hasButton || selectorInfo.hasLink)) {
64
+ this.#deadClickSetup(this.#aggregationEvent)
65
+ this.#errorClickSetup()
66
+ }
42
67
  return finishedEvent
43
68
  }
44
69
  }
45
- }
46
70
 
47
- /**
48
- * Generates a selector path for the event, starting with simple cases like window or document and getting more complex for dom-tree traversals as needed.
49
- * Will return a random selector path value if no other path can be determined, to force the aggregator to skip aggregation for this event.
50
- * @param {Event} evt
51
- * @returns {string}
52
- */
53
- function getSelectorPath (evt, targetFields) {
54
- let selectorPath; let nearestTargetFields = {}
55
- if (OBSERVED_WINDOW_EVENTS.includes(evt.type) || evt.target === window) selectorPath = 'window'
56
- else if (evt.target === document) selectorPath = 'document'
57
- // if still no selectorPath, generate one from target tree that includes elem ids
58
- else {
59
- const { path, nearestFields } = generateSelectorPath(evt.target, targetFields)
60
- selectorPath = path
61
- nearestTargetFields = nearestFields
71
+ markAsErrorClick () {
72
+ if (this.#aggregationEvent && this.#errorClickTimer) {
73
+ this.#aggregationEvent.errorClick = true
74
+ this.#errorClickCleanup()
75
+ }
76
+ }
77
+
78
+ #errorClickSetup () {
79
+ this.#errorClickTimer = new Timer({
80
+ onEnd: () => {
81
+ this.#errorClickCleanup()
82
+ }
83
+ }, FRUSTRATION_TIMEOUT_MS)
84
+ }
85
+
86
+ #errorClickCleanup () {
87
+ this.#errorClickTimer?.clear()
88
+ this.#errorClickTimer = undefined
89
+ }
90
+
91
+ #deadClickSetup (userAction) {
92
+ if (this.#isEvaluatingDeadClick() || !this.#domObserver) return
93
+
94
+ this.#domObserver.observe(document, {
95
+ attributes: true,
96
+ characterData: true,
97
+ childList: true,
98
+ subtree: true
99
+ })
100
+ this.#deadClickTimer = new Timer({
101
+ onEnd: () => {
102
+ userAction.deadClick = true
103
+ this.#deadClickCleanup()
104
+ }
105
+ }, FRUSTRATION_TIMEOUT_MS)
106
+ }
107
+
108
+ #deadClickCleanup () {
109
+ this.#domObserver?.disconnect()
110
+ this.#deadClickTimer?.clear()
111
+ this.#deadClickTimer = undefined
112
+ }
113
+
114
+ #isEvaluatingDeadClick () {
115
+ return this.#deadClickTimer !== undefined
116
+ }
117
+
118
+ isLiveClick () {
119
+ if (this.#isEvaluatingDeadClick()) this.#deadClickCleanup()
62
120
  }
63
- // if STILL no selectorPath, it will return undefined which will skip aggregation for this event
64
- return { selectorPath, nearestTargetFields }
65
121
  }
66
122
 
67
123
  /**
@@ -12,6 +12,8 @@ export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur']
12
12
  export const RAGE_CLICK_THRESHOLD_EVENTS = 4
13
13
  export const RAGE_CLICK_THRESHOLD_MS = 1000
14
14
 
15
+ export const FRUSTRATION_TIMEOUT_MS = 2000
16
+
15
17
  export const RESERVED_EVENT_TYPES = ['PageAction', 'UserAction', 'BrowserPerformance']
16
18
 
17
19
  export const FEATURE_FLAGS = {
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { globalScope, isBrowserScope } from '../../../common/constants/runtime'
7
7
  import { handle } from '../../../common/event-emitter/handle'
8
- import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
8
+ import { eventListenerOpts, windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
9
9
  import { debounce } from '../../../common/util/invoke'
10
10
  import { setupAddPageActionAPI } from '../../../loaders/api/addPageAction'
11
11
  import { setupFinishedAPI } from '../../../loaders/api/finished'
@@ -14,6 +14,12 @@ import { setupRegisterAPI } from '../../../loaders/api/register'
14
14
  import { setupMeasureAPI } from '../../../loaders/api/measure'
15
15
  import { InstrumentBase } from '../../utils/instrument-base'
16
16
  import { FEATURE_NAME, OBSERVED_EVENTS, OBSERVED_WINDOW_EVENTS } from '../constants'
17
+ import { FEATURE_NAMES } from '../../../loaders/features/features'
18
+ import { wrapHistory } from '../../../common/wrap/wrap-history'
19
+ import { wrapFetch } from '../../../common/wrap/wrap-fetch'
20
+ import { wrapXhr } from '../../../common/wrap/wrap-xhr'
21
+ import { parseUrl } from '../../../common/url/parse-url'
22
+ import { extractUrl } from '../../../common/url/extract-url'
17
23
 
18
24
  export class Instrument extends InstrumentBase {
19
25
  static featureName = FEATURE_NAME
@@ -55,6 +61,50 @@ export class Instrument extends InstrumentBase {
55
61
  })
56
62
  observer.observe({ type: 'resource', buffered: true })
57
63
  }
64
+
65
+ const historyEE = wrapHistory(this.ee)
66
+ historyEE.on('pushState-end', navigationChange)
67
+ historyEE.on('replaceState-end', navigationChange)
68
+ window.addEventListener('hashchange', navigationChange, eventListenerOpts(true, this.removeOnAbort?.signal))
69
+ window.addEventListener('popstate', navigationChange, eventListenerOpts(true, this.removeOnAbort?.signal))
70
+ function navigationChange () {
71
+ historyEE.emit('navChange')
72
+ }
73
+ }
74
+
75
+ try {
76
+ this.removeOnAbort = new AbortController()
77
+ } catch (e) {}
78
+
79
+ this.abortHandler = () => {
80
+ this.removeOnAbort?.abort()
81
+ this.abortHandler = undefined // weakly allow this abort op to run only once
82
+ }
83
+
84
+ globalScope.addEventListener('error', () => {
85
+ handle('uaErr', [], undefined, FEATURE_NAMES.genericEvents, this.ee)
86
+ }, eventListenerOpts(false, this.removeOnAbort?.signal))
87
+
88
+ wrapFetch(this.ee)
89
+ wrapXhr(this.ee)
90
+ this.ee.on('open-xhr-start', (args, xhr) => {
91
+ if (!isInternalTraffic(args[1])) {
92
+ xhr.addEventListener('readystatechange', () => {
93
+ if (xhr.readyState === 2) { // HEADERS_RECEIVED
94
+ handle('uaXhr', [], undefined, FEATURE_NAMES.genericEvents, this.ee)
95
+ }
96
+ })
97
+ }
98
+ })
99
+ this.ee.on('fetch-start', (fetchArguments) => {
100
+ if (fetchArguments.length >= 1 && !isInternalTraffic(extractUrl(fetchArguments[0]))) {
101
+ handle('uaXhr', [], undefined, FEATURE_NAMES.genericEvents, this.ee)
102
+ }
103
+ })
104
+
105
+ function isInternalTraffic (url) {
106
+ const parsedUrl = parseUrl(url)
107
+ return agentRef.beacons.includes(parsedUrl.hostname + ':' + parsedUrl.port)
58
108
  }
59
109
 
60
110
  /** If any of the sources are active, import the aggregator. otherwise deregister */
@@ -28,6 +28,7 @@ const FRAMEWORKS = {
28
28
  JQUERY: 'Jquery',
29
29
  MOOTOOLS: 'MooTools',
30
30
  QWIK: 'Qwik',
31
+ FLUTTER: 'Flutter',
31
32
 
32
33
  ELECTRON: 'Electron'
33
34
  }
@@ -71,6 +72,7 @@ export function getFrameworks () {
71
72
  if (Object.prototype.hasOwnProperty.call(window, 'jQuery')) frameworks.push(FRAMEWORKS.JQUERY)
72
73
  if (Object.prototype.hasOwnProperty.call(window, 'MooTools')) frameworks.push(FRAMEWORKS.MOOTOOLS)
73
74
  if (Object.prototype.hasOwnProperty.call(window, 'qwikevents')) frameworks.push(FRAMEWORKS.QWIK)
75
+ if (Object.hasOwn(window, '_flutter')) frameworks.push(FRAMEWORKS.FLUTTER)
74
76
 
75
77
  if (detectElectron()) frameworks.push(FRAMEWORKS.ELECTRON)
76
78
  } catch (err) {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Copyright 2020-2025 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ export function evaluateHarvestMetadata (pageMetadata) {
7
+ try {
8
+ const supportabilityTags = []
9
+
10
+ // Report SM like... audit/<feature_name>/<hasReplay|hasTrace|hasError>/<true|false>/<negative|positive>
11
+ const formTag = (...strings) => strings.join('/')
12
+
13
+ // Track if replay/trace/error harvests actually occurred (key only exists when harvested)
14
+ function evaluateTag (feature, flag, hasFlag, hasHarvest) {
15
+ const AUDIT = 'audit'
16
+ if (hasFlag) {
17
+ // False positive: flag true, but no harvest
18
+ if (!hasHarvest) supportabilityTags.push(formTag(AUDIT, feature, flag, 'false', 'positive'))
19
+ // True positive (correct)
20
+ else supportabilityTags.push(formTag(AUDIT, feature, flag, 'true', 'positive'))
21
+ } else {
22
+ // False negative: flag false, but harvest occurred
23
+ if (hasHarvest) supportabilityTags.push(formTag(AUDIT, feature, flag, 'false', 'negative'))
24
+ // True negative (correct)
25
+ else supportabilityTags.push(formTag(AUDIT, feature, flag, 'true', 'negative'))
26
+ }
27
+ }
28
+
29
+ if (pageMetadata.page_view_event) {
30
+ evaluateTag('page_view', 'hasReplay', pageMetadata.page_view_event.hasReplay, !!pageMetadata.session_replay)
31
+ evaluateTag('page_view', 'hasTrace', pageMetadata.page_view_event.hasTrace, !!pageMetadata.session_trace)
32
+ }
33
+
34
+ if (pageMetadata.session_replay) {
35
+ evaluateTag('session_replay', 'hasError', pageMetadata.session_replay.hasError, !!pageMetadata.jserrors)
36
+ }
37
+
38
+ return supportabilityTags
39
+ } catch (err) {
40
+ return []
41
+ }
42
+ }
@@ -11,6 +11,7 @@ import { windowAddEventListener } from '../../../common/event-listener/event-lis
11
11
  import { isBrowserScope, isWorkerScope } from '../../../common/constants/runtime'
12
12
  import { AggregateBase } from '../../utils/aggregate-base'
13
13
  import { isIFrameWindow } from '../../../common/dom/iframe'
14
+ import { evaluateHarvestMetadata } from './harvest-metadata'
14
15
  // import { WEBSOCKET_TAG } from '../../../common/wrap/wrap-websocket'
15
16
  // import { handleWebsocketEvents } from './websocket-detection'
16
17
 
@@ -19,6 +20,15 @@ export class Aggregate extends AggregateBase {
19
20
  constructor (agentRef) {
20
21
  super(agentRef, FEATURE_NAME)
21
22
  this.harvestOpts.aggregatorTypes = ['cm', 'sm'] // the types in EventAggregator this feature cares about
23
+
24
+ /** all the harvest metadata metrics need to be evaluated simulataneously at unload time so just temporarily buffer them and dont make SMs immediately from the data */
25
+ this.harvestMetadata = {}
26
+ this.harvestOpts.beforeUnload = () => {
27
+ evaluateHarvestMetadata(this.harvestMetadata).forEach(smTag => {
28
+ this.storeSupportabilityMetrics(smTag)
29
+ })
30
+ }
31
+
22
32
  // This feature only harvests once per potential EoL of the page, which is handled by the central harvester.
23
33
 
24
34
  // this must be read/stored synchronously, as the currentScript is removed from the DOM after this script is executed and this lookup will be void
@@ -126,6 +136,17 @@ export class Aggregate extends AggregateBase {
126
136
  // handleWebsocketEvents(this.storeSupportabilityMetrics.bind(this), tag, ...args)
127
137
  // }, this.featureName, this.ee)
128
138
  // })
139
+
140
+ /** all the harvest metadata metrics need to be evaluated simulataneously at unload time so just temporarily buffer them and dont make SMs immediately from the data */
141
+ registerHandler('harvest-metadata', (harvestMetadataObject = {}) => {
142
+ try {
143
+ Object.keys(harvestMetadataObject).forEach(key => {
144
+ Object.assign(this.harvestMetadata[key] ??= {}, harvestMetadataObject[key])
145
+ })
146
+ } catch (e) {
147
+ // failed to merge harvest metadata... ignore
148
+ }
149
+ }, this.featureName, this.ee)
129
150
  }
130
151
 
131
152
  eachSessionChecks () {
@@ -136,7 +136,7 @@ export class Aggregate extends AggregateBase {
136
136
  if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(TRIGGERS.SWITCH_TO_FULL, this.mode) // off --> full
137
137
  this.syncWithSessionManager({ sessionReplayMode: this.mode })
138
138
  } else {
139
- this.initializeRecording(MODE.FULL, true)
139
+ this.initializeRecording(MODE.FULL, true, TRIGGERS.SWITCH_TO_FULL)
140
140
  }
141
141
  }
142
142
 
@@ -57,7 +57,11 @@ export class Recorder {
57
57
  this.stopRecording()
58
58
  }, this.srFeatureName, this.ee)
59
59
 
60
- registerHandler(RRWEB_DATA_CHANNEL, (event, isCheckout) => { this.audit(event, isCheckout) }, this.srFeatureName, this.ee)
60
+ /** If Agg is already drained before importing the recorder (likely deferred API call pattern),
61
+ * registerHandler wont do anything. Just set up the on listener directly */
62
+ const processReplayNode = (event, isCheckout) => { this.audit(event, isCheckout) }
63
+ if (this.srInstrument.featAggregate?.drained) this.ee.on(RRWEB_DATA_CHANNEL, processReplayNode)
64
+ else registerHandler(RRWEB_DATA_CHANNEL, processReplayNode, this.srFeatureName, this.ee)
61
65
  }
62
66
 
63
67
  get trigger () {
@@ -15,4 +15,5 @@ export function setupNoticeErrorAPI (agent) {
15
15
  export function noticeError (err, customAttributes, agentRef, targetEntityGuid, timestamp = now()) {
16
16
  if (typeof err === 'string') err = new Error(err)
17
17
  handle('err', [err, timestamp, false, customAttributes, agentRef.runtime.isRecording, undefined, targetEntityGuid], undefined, FEATURE_NAMES.jserrors, agentRef.ee)
18
+ handle('uaErr', [], undefined, FEATURE_NAMES.genericEvents, agentRef.ee)
18
19
  }
@@ -50,6 +50,7 @@ export function configure (agent, opts = {}, loaderType, forceDrain) {
50
50
  internalTrafficList.push(updatedInit.proxy.assets)
51
51
  }
52
52
  if (updatedInit.proxy.beacon) internalTrafficList.push(updatedInit.proxy.beacon)
53
+ agent.beacons = [...internalTrafficList]
53
54
 
54
55
  setTopLevelCallers(agent) // no need to set global APIs on newrelic obj more than once
55
56
  addToNREUM('activatedFeatures', activatedFeatures)