@newrelic/browser-agent 1.312.1 → 1.313.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 (104) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/common/config/init-types.js +3 -2
  3. package/dist/cjs/common/config/init.js +10 -8
  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/timing/time-keeper.js +28 -1
  7. package/dist/cjs/common/util/short-circuit.js +13 -0
  8. package/dist/cjs/common/util/submit-data.js +2 -9
  9. package/dist/cjs/common/v2/script-correlation.js +50 -0
  10. package/dist/cjs/common/v2/script-tracker.js +278 -0
  11. package/dist/cjs/common/{util/v2.js → v2/utils.js} +4 -4
  12. package/dist/cjs/common/wrap/wrap-fetch.js +2 -2
  13. package/dist/cjs/common/wrap/wrap-function.js +2 -2
  14. package/dist/cjs/common/wrap/wrap-xhr.js +2 -2
  15. package/dist/cjs/features/ajax/aggregate/index.js +4 -4
  16. package/dist/cjs/features/generic_events/aggregate/index.js +21 -2
  17. package/dist/cjs/features/generic_events/instrument/index.js +24 -21
  18. package/dist/cjs/features/jserrors/aggregate/index.js +206 -40
  19. package/dist/cjs/features/logging/aggregate/index.js +4 -4
  20. package/dist/cjs/features/metrics/instrument/index.js +1 -8
  21. package/dist/cjs/features/session_trace/aggregate/index.js +3 -4
  22. package/dist/cjs/features/session_trace/aggregate/trace/storage.js +2 -2
  23. package/dist/cjs/interfaces/registered-entity.js +7 -20
  24. package/dist/cjs/loaders/api/register-api-types.js +8 -8
  25. package/dist/cjs/loaders/api/register.js +49 -43
  26. package/dist/esm/common/config/init-types.js +3 -2
  27. package/dist/esm/common/config/init.js +10 -8
  28. package/dist/esm/common/constants/env.cdn.js +1 -1
  29. package/dist/esm/common/constants/env.npm.js +1 -1
  30. package/dist/esm/common/timing/time-keeper.js +28 -1
  31. package/dist/esm/common/util/short-circuit.js +6 -0
  32. package/dist/esm/common/util/submit-data.js +2 -9
  33. package/dist/esm/common/v2/script-correlation.js +43 -0
  34. package/dist/esm/common/v2/script-tracker.js +270 -0
  35. package/dist/esm/common/{util/v2.js → v2/utils.js} +4 -4
  36. package/dist/esm/common/wrap/wrap-fetch.js +1 -1
  37. package/dist/esm/common/wrap/wrap-function.js +1 -1
  38. package/dist/esm/common/wrap/wrap-xhr.js +1 -1
  39. package/dist/esm/features/ajax/aggregate/index.js +1 -1
  40. package/dist/esm/features/generic_events/aggregate/index.js +20 -1
  41. package/dist/esm/features/generic_events/instrument/index.js +24 -21
  42. package/dist/esm/features/jserrors/aggregate/index.js +205 -39
  43. package/dist/esm/features/logging/aggregate/index.js +1 -1
  44. package/dist/esm/features/metrics/instrument/index.js +2 -9
  45. package/dist/esm/features/session_trace/aggregate/index.js +4 -5
  46. package/dist/esm/features/session_trace/aggregate/trace/storage.js +2 -2
  47. package/dist/esm/interfaces/registered-entity.js +7 -20
  48. package/dist/esm/loaders/api/register-api-types.js +8 -8
  49. package/dist/esm/loaders/api/register.js +46 -41
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/dist/types/common/config/init-types.d.ts +10 -8
  52. package/dist/types/common/config/init-types.d.ts.map +1 -1
  53. package/dist/types/common/config/init.d.ts.map +1 -1
  54. package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
  55. package/dist/types/common/util/short-circuit.d.ts +8 -0
  56. package/dist/types/common/util/short-circuit.d.ts.map +1 -0
  57. package/dist/types/common/util/submit-data.d.ts.map +1 -1
  58. package/dist/types/common/v2/script-correlation.d.ts +38 -0
  59. package/dist/types/common/v2/script-correlation.d.ts.map +1 -0
  60. package/dist/types/common/{util → v2}/script-tracker.d.ts +3 -0
  61. package/dist/types/common/v2/script-tracker.d.ts.map +1 -0
  62. package/dist/types/common/{util/v2.d.ts → v2/utils.d.ts} +1 -1
  63. package/dist/types/common/v2/utils.d.ts.map +1 -0
  64. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  65. package/dist/types/features/generic_events/instrument/index.d.ts +1 -1
  66. package/dist/types/features/generic_events/instrument/index.d.ts.map +1 -1
  67. package/dist/types/features/jserrors/aggregate/index.d.ts +25 -16
  68. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  69. package/dist/types/features/metrics/instrument/index.d.ts.map +1 -1
  70. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  71. package/dist/types/interfaces/registered-entity.d.ts +0 -10
  72. package/dist/types/interfaces/registered-entity.d.ts.map +1 -1
  73. package/dist/types/loaders/api/register-api-types.d.ts +15 -15
  74. package/dist/types/loaders/api/register-api-types.d.ts.map +1 -1
  75. package/dist/types/loaders/api/register.d.ts +6 -0
  76. package/dist/types/loaders/api/register.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/common/config/init-types.js +3 -2
  79. package/src/common/config/init.js +6 -4
  80. package/src/common/timing/time-keeper.js +30 -1
  81. package/src/common/util/short-circuit.js +6 -0
  82. package/src/common/util/submit-data.js +2 -8
  83. package/src/common/v2/script-correlation.js +44 -0
  84. package/src/common/v2/script-tracker.js +260 -0
  85. package/src/common/{util/v2.js → v2/utils.js} +4 -4
  86. package/src/common/wrap/wrap-fetch.js +1 -1
  87. package/src/common/wrap/wrap-function.js +1 -1
  88. package/src/common/wrap/wrap-xhr.js +1 -1
  89. package/src/features/ajax/aggregate/index.js +1 -1
  90. package/src/features/generic_events/aggregate/index.js +20 -1
  91. package/src/features/generic_events/instrument/index.js +25 -22
  92. package/src/features/jserrors/aggregate/index.js +200 -43
  93. package/src/features/logging/aggregate/index.js +1 -1
  94. package/src/features/metrics/instrument/index.js +2 -13
  95. package/src/features/session_trace/aggregate/index.js +4 -6
  96. package/src/features/session_trace/aggregate/trace/storage.js +2 -2
  97. package/src/interfaces/registered-entity.js +8 -20
  98. package/src/loaders/api/register-api-types.js +8 -8
  99. package/src/loaders/api/register.js +42 -34
  100. package/dist/cjs/common/util/script-tracker.js +0 -204
  101. package/dist/esm/common/util/script-tracker.js +0 -196
  102. package/dist/types/common/util/script-tracker.d.ts.map +0 -1
  103. package/dist/types/common/util/v2.d.ts.map +0 -1
  104. package/src/common/util/script-tracker.js +0 -189
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.312.1",
3
+ "version": "1.313.0",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -12,8 +12,9 @@
12
12
  * @property {boolean} [ajax.enabled] - Turn on/off the ajax feature (on by default).
13
13
  * @property {boolean} [ajax.autoStart] - If true, the agent will automatically start the ajax feature. Otherwise, it will be in a deferred state until the `start` API method is called.
14
14
  * @property {Object} [api]
15
- * @property {boolean} [api.allow_registered_children] - If true, the agent will allow registered children to be sent to the server.
16
- * @property {boolean} [api.duplicate_registered_data] - If true, the agent will capture registered child data to the main agent as well as the registered child.
15
+ * @property {Object} [api.register]
16
+ * @property {boolean} [api.register.enabled] - If true, the agent will allow registered children to be sent to the server.
17
+ * @property {boolean} [api.register.duplicate_data_to_container] - If true, the agent will capture registered child data to the main agent as well as the registered child.
17
18
  * @property {Object} [distributed_tracing]
18
19
  * @property {boolean} [distributed_tracing.enabled] - If true, distributed tracing headers will be added to outgoing requests. Requires ajax feature to be running.
19
20
  * @property {boolean} [distributed_tracing.exclude_newrelic_header]
@@ -21,7 +21,7 @@ const InitModelFn = () => {
21
21
  const hiddenState = {
22
22
  feature_flags: [],
23
23
  experimental: {
24
- allow_registered_children: false,
24
+ register: false,
25
25
  resources: false
26
26
  },
27
27
  mask_selector: '*',
@@ -49,9 +49,11 @@ const InitModelFn = () => {
49
49
  return {
50
50
  ajax: { deny_list: undefined, block_internal: true, enabled: true, autoStart: true },
51
51
  api: {
52
- get allow_registered_children () { return hiddenState.feature_flags.includes(FEATURE_FLAGS.REGISTER) || hiddenState.experimental.allow_registered_children },
53
- set allow_registered_children (val) { hiddenState.experimental.allow_registered_children = val },
54
- duplicate_registered_data: false
52
+ register: {
53
+ get enabled () { return hiddenState.feature_flags.includes(FEATURE_FLAGS.REGISTER) || hiddenState.experimental.register },
54
+ set enabled (val) { hiddenState.experimental.register = val },
55
+ duplicate_data_to_container: false
56
+ }
55
57
  },
56
58
  browser_consent_mode: { enabled: false },
57
59
  distributed_tracing: {
@@ -1,9 +1,12 @@
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
+ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants'
5
6
  import { originTime } from '../constants/runtime'
6
7
  import { isNative } from '../util/monkey-patched'
8
+ import { handle } from '../event-emitter/handle'
9
+ import { FEATURE_NAMES } from '../../loaders/features/features'
7
10
 
8
11
  /**
9
12
  * Class used to adjust the timestamp of harvested data to New Relic server time. This
@@ -37,12 +40,34 @@ export class TimeKeeper {
37
40
  */
38
41
  #ready = false
39
42
 
43
+ #reportedDrift = false
44
+
40
45
  constructor (sessionObj) {
41
46
  this.#session = sessionObj
42
47
  this.processStoredDiff()
43
48
  isNative(performance.now, Date.now) // will warn the user if these are not native functions. We need these to be native for time in the agent to be accurate in general.
44
49
  }
45
50
 
51
+ #detectDrift () {
52
+ if (this.#reportedDrift) return
53
+ try {
54
+ // Drift detection: measures if performance.now() and Date.now() have become desynchronized
55
+ // This can happen when a machine sleeps and the performance timer freezes while Date continues
56
+ // this can also happen when a user sets their clock forward during the page lifecycle,
57
+ // but we have no way of distinguishing that from actual clock drift so we will just treat it as drift.
58
+ // In either case, the performance timestamps would be inaccurate at that point so we want to detect and report a count of it.
59
+ // We only detect positive drift (performance clock falling behind Date clock)
60
+ // Note: localTimeDiff (server time offset) is NOT part of drift - that's a legitimate offset
61
+ const drift = (Date.now() - originTime) - performance.now()
62
+ if (drift > 1000) {
63
+ this.#reportedDrift = true
64
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/ClockDrift/Detected', drift], undefined, FEATURE_NAMES.metrics, this.#session.agentRef.ee)
65
+ }
66
+ } catch (err) {
67
+ // Silently ignore drift detection errors to avoid breaking normal operation
68
+ }
69
+ }
70
+
46
71
  get ready () {
47
72
  return this.#ready
48
73
  }
@@ -90,6 +115,7 @@ export class TimeKeeper {
90
115
  * @returns {number} Corrected unix/epoch timestamp
91
116
  */
92
117
  convertRelativeTimestamp (relativeTime) {
118
+ this.#detectDrift()
93
119
  return originTime + relativeTime
94
120
  }
95
121
 
@@ -100,6 +126,7 @@ export class TimeKeeper {
100
126
  * @returns {number}
101
127
  */
102
128
  convertAbsoluteTimestamp (timestamp) {
129
+ this.#detectDrift()
103
130
  return timestamp - originTime
104
131
  }
105
132
 
@@ -109,6 +136,7 @@ export class TimeKeeper {
109
136
  * @return {number} Corrected unix/epoch timestamp
110
137
  */
111
138
  correctAbsoluteTimestamp (timestamp) {
139
+ this.#detectDrift()
112
140
  return timestamp - this.#localTimeDiff
113
141
  }
114
142
 
@@ -118,6 +146,7 @@ export class TimeKeeper {
118
146
  * @returns {number}
119
147
  */
120
148
  correctRelativeTimestamp (relativeTime) {
149
+ this.#detectDrift()
121
150
  return this.correctAbsoluteTimestamp(this.convertRelativeTimestamp(relativeTime))
122
151
  }
123
152
 
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ /** A special error used to short-circuit the error processing pipeline */
6
+ export class ShortCircuit extends Error {}
@@ -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
 
@@ -59,7 +59,7 @@ export function xhrFetch ({
59
59
  objHeaders[header.key] = header.value
60
60
  }
61
61
 
62
- return fetch(url, { headers: objHeaders, method, body, credentials: 'include' })
62
+ return fetch(url, { headers: objHeaders, method, body })
63
63
  }
64
64
 
65
65
  /**
@@ -76,12 +76,6 @@ export function xhr ({ url, body = null, sync, method = 'POST', headers = [{ key
76
76
  const request = new XMLHttpRequest()
77
77
 
78
78
  request.open(method, url, !sync)
79
- try {
80
- // Set cookie
81
- if ('withCredentials' in request) request.withCredentials = true
82
- } catch (e) {
83
- // do nothing
84
- }
85
79
 
86
80
  headers.forEach(header => {
87
81
  request.setRequestHeader(header.key, header.value)
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ /**
6
+ * Represents script correlation data combining DOM and Performance API information
7
+ */
8
+ export class ScriptCorrelation {
9
+ /** @type {CorrelationTiming} [dom] - DOM-related information */
10
+ dom = new CorrelationTiming()
11
+ /** @type {CorrelationTiming} [performance] - Performance-related information */
12
+ performance = new CorrelationTiming()
13
+
14
+ /**
15
+ * Creates a new ScriptCorrelation instance
16
+ * @param {string} url - The cleaned URL of the script
17
+ */
18
+ constructor (url) {
19
+ /** @type {string} The cleaned URL of the script */
20
+ this.url = url
21
+ }
22
+
23
+ /**
24
+ * Gets the script timing, using DOM timings if available, otherwise falling back to performance timings or registeredAt as appropriate. This is used to provide the most accurate script timing possible for registered entities.
25
+ * @returns {{start: number, end: number}
26
+ */
27
+ get script () {
28
+ const start = Math.max(this.dom.start, this.performance.end)
29
+ const end = Math.max(this.dom.end, this.performance.end, start)
30
+ return {
31
+ start,
32
+ end
33
+ }
34
+ }
35
+ }
36
+
37
+ class CorrelationTiming {
38
+ /** @type {number} [start] - The startTime from the performance entry */
39
+ start = 0
40
+ /** @type {number} [end] - The responseEnd from the performance entry */
41
+ end = 0
42
+ /** @type {*} [value] - The entry value */
43
+ value = undefined
44
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { globalScope } from '../constants/runtime'
7
+ import { now } from '../timing/now'
8
+ import { cleanURL } from '../url/clean-url'
9
+ import { chrome, chromeEval, gecko } from '../util/browser-stack-matchers'
10
+ import { ScriptCorrelation } from './script-correlation'
11
+
12
+ /**
13
+ * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
14
+ */
15
+
16
+ /** export for testing purposes */
17
+ export let thisFile
18
+ try {
19
+ thisFile = extractUrlsFromStack(getDeepStackTrace())[0]
20
+ } catch (err) {
21
+ thisFile = extractUrlsFromStack(err)[0]
22
+ }
23
+
24
+ /** @type {(entry: PerformanceEntry) => boolean} - A shared function to determine if a performance entry is a valid script or link resource for evaluation */
25
+ const validEntryCriteria = entry => entry.initiatorType === 'script' || (['link', 'fetch'].includes(entry.initiatorType) && entry.name.endsWith('.js'))
26
+
27
+ /** @type {Map<string, ScriptCorrelation>} - Central registry for script correlations containing both DOM and Performance data */
28
+ export const scriptCorrelations = new Map()
29
+ /** @type {Array<{ test: (entry: PerformanceEntry) => boolean, addedAt: number }>} an array of PerformanceObserver subscribers to check for late emissions */
30
+ let poSubscribers = []
31
+
32
+ /**
33
+ * Retrieves a script correlation by URL using exact matching
34
+ * @param {string} targetUrl - The URL to find
35
+ * @returns {ScriptCorrelation | undefined} - The correlation object if found
36
+ */
37
+ function findCorrelation (targetUrl) {
38
+ return scriptCorrelations.get(targetUrl)
39
+ }
40
+
41
+ /**
42
+ * Gets or creates a script correlation entry
43
+ * @param {string} url - The cleaned URL
44
+ * @returns {ScriptCorrelation} - The correlation object
45
+ */
46
+ function getOrCreateCorrelation (url) {
47
+ const existing = findCorrelation(url)
48
+ if (existing) return existing
49
+
50
+ const correlation = new ScriptCorrelation(url)
51
+ scriptCorrelations.set(url, correlation)
52
+
53
+ // Keep size under control
54
+ if (scriptCorrelations.size > 1000) {
55
+ const firstKey = scriptCorrelations.keys().next().value
56
+ scriptCorrelations.delete(firstKey)
57
+ }
58
+
59
+ return correlation
60
+ }
61
+
62
+ /** Set up a MutationObserver to detect script elements being added to the DOM */
63
+ if (globalScope.MutationObserver && globalScope.document) {
64
+ const scriptMutationObserver = new MutationObserver((mutations) => {
65
+ mutations.forEach((mutation) => {
66
+ mutation.addedNodes.forEach((node) => {
67
+ if (node.nodeName === 'SCRIPT' && node.src) {
68
+ const cleanedSrc = cleanURL(node.src)
69
+ const correlation = getOrCreateCorrelation(cleanedSrc)
70
+
71
+ correlation.dom.start = now()
72
+ correlation.dom.value = node
73
+
74
+ const setEnd = () => { correlation.dom.end = now() }
75
+ ;['load', 'error'].forEach(event => node.addEventListener(event, setEnd, { once: true }))
76
+ }
77
+ })
78
+ })
79
+ })
80
+
81
+ scriptMutationObserver.observe(globalScope.document, {
82
+ childList: true,
83
+ subtree: true
84
+ })
85
+ }
86
+
87
+ if (globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
88
+ /** We must track the script assets this way, because the performance buffer can fill up and when it does that
89
+ * it stops accepting new entries (instead of dropping old entries), which means if the register API is called
90
+ * after the buffer fills up we won't be able to get the script timing information from the resource timing API
91
+ */
92
+ const scriptObserver = new PerformanceObserver((list) => {
93
+ list.getEntries().filter(validEntryCriteria).forEach((entry) => {
94
+ // Update correlation with performance data (creates entry if needed)
95
+ const entryUrl = cleanURL(entry.name)
96
+ const correlation = getOrCreateCorrelation(entryUrl)
97
+ correlation.performance.start = Math.floor(entry.startTime)
98
+ correlation.performance.end = Math.floor(entry.responseEnd)
99
+ correlation.performance.value = entry
100
+
101
+ // Clear resolved or expired subscribers
102
+ const canClear = []
103
+ poSubscribers.forEach(({ test, addedAt }, idx) => {
104
+ if (test(entry) || now() - addedAt > 10000) canClear.push(idx)
105
+ })
106
+ poSubscribers = poSubscribers.filter((_, idx) => !canClear.includes(idx))
107
+ })
108
+ })
109
+ scriptObserver.observe({ type: 'resource', buffered: true })
110
+ }
111
+
112
+ /**
113
+ * Extracts URLs from stack traces using the same logic as compute-stack-trace.js
114
+ * @param {string} stack The error stack trace
115
+ * @returns {string[]} Array of cleaned URLs found in the stack trace
116
+ */
117
+ export function extractUrlsFromStack (stack) {
118
+ if (!stack || typeof stack !== 'string') return []
119
+
120
+ const urls = new Set()
121
+ const lines = stack.split('\n')
122
+
123
+ for (const line of lines) {
124
+ // Try gecko format first, then chrome
125
+ const parts = line.match(gecko) || line.match(chrome) || line.match(chromeEval)
126
+ if (parts && parts[2]) {
127
+ urls.add(cleanURL(parts[2]))
128
+ } else {
129
+ // Fallback: match URLs using a generic .js pattern (non-greedy to handle ports and query params)
130
+ const fallbackMatch = line.match(/\(([^)]+\.js):\d+:\d+\)/) || line.match(/^\s+at\s+([^\s(]+\.js):\d+:\d+/)
131
+ if (fallbackMatch && fallbackMatch[1]) {
132
+ urls.add(cleanURL(fallbackMatch[1]))
133
+ }
134
+ }
135
+ }
136
+ return [...urls]
137
+ }
138
+
139
+ /**
140
+ * Returns a deep stack trace by temporarily increasing the stack trace limit.
141
+ * @returns {Error.stack | undefined}
142
+ */
143
+ export function getDeepStackTrace () {
144
+ let stack
145
+ try {
146
+ const originalStackLimit = Error.stackTraceLimit
147
+ Error.stackTraceLimit = 50
148
+ stack = new Error().stack
149
+ Error.stackTraceLimit = originalStackLimit
150
+ } catch (e) {
151
+ stack = new Error().stack
152
+ }
153
+ return stack
154
+ }
155
+
156
+ /**
157
+ * Indicates whether the provided URL matches any script preload link tags in the document.
158
+ * @param {string} targetUrl The URL to match against preload tags
159
+ * @returns {boolean} True if a matching preload link is found, false otherwise
160
+ */
161
+ function wasPreloaded (targetUrl) {
162
+ if (!targetUrl || !globalScope.document) return false
163
+
164
+ try {
165
+ const linkTags = globalScope.document.querySelectorAll('link[rel="preload"][as="script"]')
166
+ for (const link of linkTags) {
167
+ // link.href is resolved to an absolute URL by the browser (even if supplied as relative), so we can match exactly against the cleaned target URL
168
+ if (cleanURL(link.href) === targetUrl) return true
169
+ }
170
+ } catch (error) {
171
+ // Don't let DOM parsing errors break anything
172
+ }
173
+ return false
174
+ }
175
+
176
+ /**
177
+ * Checks if a performance entry matches the target MFE script URL using exact matching
178
+ * @param {PerformanceResourceTiming} entry - The resource timing entry
179
+ * @param {string} targetUrl - The MFE script URL to match
180
+ * @returns {boolean} True if the entry matches
181
+ */
182
+ function entryMatchesUrl (entry, targetUrl) {
183
+ const entryUrl = cleanURL(entry.name)
184
+ return entryUrl === targetUrl
185
+ }
186
+
187
+ /**
188
+ * Applies performance entry timing data to a timings object
189
+ * @param {RegisterAPITimings} timings - The timings object to update
190
+ * @param {PerformanceResourceTiming} entry - The performance entry
191
+ */
192
+ function applyPerformanceEntry (timings, entry) {
193
+ timings.fetchStart = Math.floor(entry.startTime)
194
+ timings.fetchEnd = Math.floor(entry.responseEnd)
195
+ timings.asset = entry.name
196
+ timings.type = entry.initiatorType
197
+ }
198
+
199
+ /**
200
+ * Uses the stack of the initiator function, returns script timing information if a script can be found with the resource timing API matching the URL found in the stack.
201
+ * @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
202
+ */
203
+ export function findScriptTimings () {
204
+ const timings = { registeredAt: now(), reportedAt: undefined, fetchStart: 0, fetchEnd: 0, scriptStart: 0, scriptEnd: 0, asset: undefined, type: 'unknown' }
205
+ const stack = getDeepStackTrace()
206
+ if (!stack) return timings
207
+
208
+ const navUrl = globalScope.performance?.getEntriesByType('navigation')?.[0]?.name || ''
209
+
210
+ try {
211
+ const urls = extractUrlsFromStack(stack)
212
+ // Filter out agent file from URLs (unless it's the only one)
213
+ const mfeScriptUrl = (urls.length > 1 ? urls.filter(line => thisFile !== line) : urls)[0]
214
+ if (!mfeScriptUrl) return timings
215
+
216
+ // Check for inline script
217
+ if (navUrl.includes(mfeScriptUrl)) {
218
+ timings.asset = cleanURL(navUrl)
219
+ timings.type = 'inline'
220
+ return timings
221
+ }
222
+
223
+ // Get correlation data
224
+ timings.correlation = findCorrelation(mfeScriptUrl)
225
+
226
+ // Use correlation's performance entry if available, otherwise check live performance API
227
+ const performanceEntry = timings.correlation?.performance.value || performance.getEntriesByType('resource').find(e => entryMatchesUrl(e, mfeScriptUrl))
228
+
229
+ if (performanceEntry) {
230
+ applyPerformanceEntry(timings, performanceEntry)
231
+ } else if (wasPreloaded(mfeScriptUrl)) {
232
+ // Handle preloaded scripts that may report late
233
+ timings.asset = mfeScriptUrl
234
+ timings.type = 'preload'
235
+
236
+ // Subscribe to late performance observer callbacks
237
+ poSubscribers.push({
238
+ addedAt: now(),
239
+ test: (entry) => {
240
+ if (entryMatchesUrl(entry, mfeScriptUrl)) {
241
+ applyPerformanceEntry(timings, entry)
242
+ return true
243
+ }
244
+ return false
245
+ }
246
+ })
247
+ }
248
+
249
+ /*
250
+ * Use getters here because the correlation data may arrive after this function returns the timing object, and we want to provide the most up-to-date timing information possible when the getters are accessed at harvest time.
251
+ * The getters will fall back to fetchEnd if correlation data isn't available yet, which is our best approximation for script execution start when actual script timings can not be determined.
252
+ */
253
+ Object.defineProperty(timings, 'scriptStart', { get: () => timings.correlation?.script.start || timings.fetchEnd })
254
+ Object.defineProperty(timings, 'scriptEnd', { get: () => timings.correlation?.script.end || timings.registeredAt })
255
+ } catch (error) {
256
+ // Don't let stack parsing errors break anything
257
+ }
258
+
259
+ return timings
260
+ }
@@ -23,7 +23,7 @@ export const V2_TYPES = {
23
23
  * @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
24
24
  */
25
25
  export function getRegisteredTargetsFromId (id, agentRef) {
26
- if (!id || !agentRef?.init.api.allow_registered_children) return []
26
+ if (!id || !agentRef?.init.api.register.enabled) return []
27
27
  const registeredEntities = agentRef.runtime.registeredEntities
28
28
  return registeredEntities?.filter(entity => String(entity.metadata.target.id) === String(id)).map(entity => entity.metadata.target) || []
29
29
  }
@@ -35,7 +35,7 @@ export function getRegisteredTargetsFromId (id, agentRef) {
35
35
  * @returns {import("../../interfaces/registered-entity").RegisterAPIMetadataTarget[]}
36
36
  */
37
37
  export function getRegisteredTargetsFromFilename (filename, agentRef) {
38
- if (!filename || !agentRef?.init.api.allow_registered_children) return []
38
+ if (!filename || !agentRef?.init.api.register.enabled) return []
39
39
  const registeredEntities = agentRef.runtime.registeredEntities
40
40
  return registeredEntities?.filter(entity => entity.metadata.timings?.asset?.endsWith(filename)).map(entity => entity.metadata.target) || []
41
41
  }
@@ -83,7 +83,7 @@ export function getVersion2DuplicationAttributes (target, aggregateInstance) {
83
83
  * @returns {boolean} returns true if the event should be duplicated for the target, false otherwise
84
84
  */
85
85
  export function shouldDuplicate (target, aggregateInstance) {
86
- return !!target && !!supportsV2(aggregateInstance) && aggregateInstance.agentRef.init.api.duplicate_registered_data
86
+ return !!target && !!supportsV2(aggregateInstance) && aggregateInstance.agentRef.init.api.register.duplicate_data_to_container
87
87
  }
88
88
 
89
89
  /**
@@ -92,7 +92,7 @@ export function shouldDuplicate (target, aggregateInstance) {
92
92
  * @returns {Array} An array of targets found from the stack trace. If no targets are found or allowed, returns an array with undefined.
93
93
  */
94
94
  export function findTargetsFromStackTrace (agentRef) {
95
- if (!agentRef?.init.api.allow_registered_children) return [undefined]
95
+ if (!agentRef?.init.api.register.enabled) return [undefined]
96
96
 
97
97
  const targets = []
98
98
  try {
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { ee as baseEE, contextId } from '../event-emitter/contextual-ee'
11
11
  import { globalScope } from '../constants/runtime'
12
- import { findTargetsFromStackTrace } from '../util/v2'
12
+ import { findTargetsFromStackTrace } from '../v2/utils'
13
13
 
14
14
  var prefix = 'fetch-'
15
15
  var bodyPrefix = prefix + 'body-'
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { ee } from '../event-emitter/contextual-ee'
11
11
  import { bundleId } from '../ids/bundle-id'
12
- import { findTargetsFromStackTrace } from '../util/v2'
12
+ import { findTargetsFromStackTrace } from '../v2/utils'
13
13
 
14
14
  export const flag = `nr@original:${bundleId}`
15
15
  const LONG_TASK_THRESHOLD = 50
@@ -14,7 +14,7 @@ import { eventListenerOpts } from '../event-listener/event-listener-opts'
14
14
  import { createWrapperWithEmitter as wfn } from './wrap-function'
15
15
  import { globalScope } from '../constants/runtime'
16
16
  import { warn } from '../util/console'
17
- import { findTargetsFromStackTrace } from '../util/v2'
17
+ import { findTargetsFromStackTrace } from '../v2/utils'
18
18
 
19
19
  const wrapped = {}
20
20
  const XHR_PROPS = ['open', 'send'] // these are the specific funcs being wrapped on all XMLHttpRequests(.prototype)
@@ -12,7 +12,7 @@ import { AggregateBase } from '../../utils/aggregate-base'
12
12
  import { parseGQL } from './gql'
13
13
  import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
14
14
  import { gosNREUMOriginals } from '../../../common/window/nreum'
15
- import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/util/v2'
15
+ import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils'
16
16
 
17
17
  export class Aggregate extends AggregateBase {
18
18
  static featureName = FEATURE_NAME
@@ -14,7 +14,7 @@ import { applyFnToProps } from '../../../common/util/traverse'
14
14
  import { UserActionsAggregator } from './user-actions/user-actions-aggregator'
15
15
  import { isIFrameWindow } from '../../../common/dom/iframe'
16
16
  import { isPureObject } from '../../../common/util/type-check'
17
- import { getVersion2Attributes } from '../../../common/util/v2'
17
+ import { getVersion2Attributes } from '../../../common/v2/utils'
18
18
 
19
19
  export class Aggregate extends AggregateBase {
20
20
  static featureName = FEATURE_NAME
@@ -259,6 +259,25 @@ export class Aggregate extends AggregateBase {
259
259
  this.addEvent(event)
260
260
  }, this.featureName, this.ee)
261
261
  }
262
+ if (!agentRef.init.feature_flags.includes('no_spv')) {
263
+ registerHandler('spv', (evt) => {
264
+ this.addEvent({
265
+ eventType: 'SecurityPolicyViolation',
266
+ timestamp: this.#toEpoch(evt.timeStamp),
267
+ blockedUri: evt.blockedURI,
268
+ documentUri: evt.documentURI,
269
+ effectiveDirective: evt.effectiveDirective,
270
+ originalPolicy: evt.originalPolicy,
271
+ sourceFile: evt.sourceFile,
272
+ statusCode: evt.statusCode,
273
+ lineNumber: evt.lineNumber,
274
+ columnNumber: evt.columnNumber,
275
+ disposition: evt.disposition,
276
+ sample: evt.sample,
277
+ referrer: evt.referrer
278
+ })
279
+ }, this.featureName, this.ee)
280
+ }
262
281
 
263
282
  this.drain()
264
283
  })
@@ -27,6 +27,7 @@ export class Instrument extends InstrumentBase {
27
27
  constructor (agentRef) {
28
28
  super(agentRef, FEATURE_NAME)
29
29
  const websocketsEnabled = agentRef.init.feature_flags.includes('websockets')
30
+ const securityPolicyViolationEnabled = !agentRef.init.feature_flags.includes('no_spv')
30
31
 
31
32
  /** config values that gate whether the generic events aggregator should be imported at all */
32
33
  const genericEventSourceConfigs = [
@@ -35,7 +36,8 @@ export class Instrument extends InstrumentBase {
35
36
  agentRef.init.performance.capture_measures,
36
37
  agentRef.init.performance.resources.enabled,
37
38
  agentRef.init.user_actions.enabled,
38
- websocketsEnabled
39
+ websocketsEnabled,
40
+ securityPolicyViolationEnabled
39
41
  ]
40
42
 
41
43
  /** feature specific APIs */
@@ -45,8 +47,24 @@ export class Instrument extends InstrumentBase {
45
47
  setupRegisterAPI(agentRef)
46
48
  setupMeasureAPI(agentRef)
47
49
 
48
- let historyEE, websocketsEE
49
- if (websocketsEnabled) websocketsEE = wrapWebSocket(this.ee)
50
+ this.removeOnAbort = new AbortController()
51
+ this.abortHandler = () => {
52
+ this.removeOnAbort.abort()
53
+ this.abortHandler = undefined // weakly allow this abort op to run only once
54
+ }
55
+
56
+ let historyEE
57
+ if (websocketsEnabled) { // this can apply outside browser scope such as in worker
58
+ const websocketsEE = wrapWebSocket(this.ee)
59
+ websocketsEE.on('ws', (nrData) => {
60
+ handle('ws-complete', [nrData], undefined, this.featureName, this.ee)
61
+ })
62
+ }
63
+ if (securityPolicyViolationEnabled) {
64
+ globalScope.addEventListener('securitypolicyviolation', (evt) => {
65
+ handle('spv', [evt], undefined, FEATURE_NAMES.genericEvents, this.ee)
66
+ }, eventListenerOpts(false, this.removeOnAbort.signal))
67
+ }
50
68
  if (isBrowserScope) {
51
69
  wrapFetch(this.ee, agentRef)
52
70
  wrapXhr(this.ee, agentRef)
@@ -65,7 +83,7 @@ export class Instrument extends InstrumentBase {
65
83
 
66
84
  globalScope.addEventListener('error', () => {
67
85
  handle('uaErr', [], undefined, FEATURE_NAMES.genericEvents, this.ee)
68
- }, eventListenerOpts(false, this.removeOnAbort?.signal))
86
+ }, eventListenerOpts(false, this.removeOnAbort.signal))
69
87
 
70
88
  this.ee.on('open-xhr-start', (args, xhr) => {
71
89
  if (!isInternalTraffic(args[1])) {
@@ -73,7 +91,7 @@ export class Instrument extends InstrumentBase {
73
91
  if (xhr.readyState === 2) { // HEADERS_RECEIVED
74
92
  handle('uaXhr', [], undefined, FEATURE_NAMES.genericEvents, this.ee)
75
93
  }
76
- })
94
+ }, eventListenerOpts(undefined, this.removeOnAbort.signal))
77
95
  }
78
96
  })
79
97
  this.ee.on('fetch-start', (fetchArguments) => {
@@ -89,8 +107,8 @@ export class Instrument extends InstrumentBase {
89
107
 
90
108
  historyEE.on('pushState-end', navigationChange)
91
109
  historyEE.on('replaceState-end', navigationChange)
92
- window.addEventListener('hashchange', navigationChange, eventListenerOpts(true, this.removeOnAbort?.signal))
93
- window.addEventListener('popstate', navigationChange, eventListenerOpts(true, this.removeOnAbort?.signal))
110
+ window.addEventListener('hashchange', navigationChange, eventListenerOpts(true, this.removeOnAbort.signal))
111
+ window.addEventListener('popstate', navigationChange, eventListenerOpts(true, this.removeOnAbort.signal))
94
112
 
95
113
  function navigationChange () {
96
114
  historyEE.emit('navChange')
@@ -109,21 +127,6 @@ export class Instrument extends InstrumentBase {
109
127
  })
110
128
  }
111
129
  }
112
- if (websocketsEnabled) { // this can apply outside browser scope such as in worker
113
- websocketsEE.on('ws', (nrData) => {
114
- handle('ws-complete', [nrData], undefined, this.featureName, this.ee)
115
- })
116
- }
117
-
118
- try {
119
- this.removeOnAbort = new AbortController()
120
- } catch (e) {}
121
-
122
- this.abortHandler = () => {
123
- this.removeOnAbort?.abort()
124
- this.abortHandler = undefined // weakly allow this abort op to run only once
125
- }
126
-
127
130
  /** If any of the sources are active, import the aggregator. otherwise deregister */
128
131
  if (genericEventSourceConfigs.some(x => x)) this.importAggregator(agentRef, () => import(/* webpackChunkName: "generic_events-aggregate" */ '../aggregate'))
129
132
  else this.deregisterDrain()