@newrelic/browser-agent 1.312.1 → 1.313.0-rc.1

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 +57 -44
  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 +54 -42
  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 +2 -2
  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 +56 -35
  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
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { handle } from '../../common/event-emitter/handle'
6
6
  import { warn } from '../../common/util/console'
7
- import { V2_TYPES } from '../../common/util/v2'
7
+ import { V2_TYPES } from '../../common/v2/utils'
8
8
  import { FEATURE_NAMES } from '../features/features'
9
9
  import { now } from '../../common/timing/now'
10
10
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants'
@@ -17,7 +17,7 @@ import { single } from '../../common/util/invoke'
17
17
  import { measure } from './measure'
18
18
  import { recordCustomEvent } from './recordCustomEvent'
19
19
  import { subscribeToPageUnload } from '../../common/window/page-visibility'
20
- import { findScriptTimings } from '../../common/util/script-tracker'
20
+ import { findScriptTimings } from '../../common/v2/script-tracker'
21
21
  import { generateRandomHexString } from '../../common/ids/unique-id'
22
22
 
23
23
  /**
@@ -26,6 +26,29 @@ import { generateRandomHexString } from '../../common/ids/unique-id'
26
26
 
27
27
  const PROTECTED_KEYS = ['name', 'id', 'type']
28
28
 
29
+ /**
30
+ * Map of API methods to their names (prevents minification from breaking method name references)
31
+ * @private
32
+ */
33
+ const METHOD_NAMES = new Map([
34
+ [addPageAction, 'addPageAction'],
35
+ [log, 'log'],
36
+ [measure, 'measure'],
37
+ [noticeError, 'noticeError'],
38
+ [recordCustomEvent, 'recordCustomEvent']
39
+ ])
40
+
41
+ /**
42
+ * Warning functions that only fire once - can be reset in tests
43
+ * @private
44
+ */
45
+ export const warnings = {
46
+ experimental: single(() => warn(54, 'newrelic.register')),
47
+ disabled: single(() => warn(55)),
48
+ invalidTarget: single((target) => warn(48, target)),
49
+ deregistered: single(() => warn(68))
50
+ }
51
+
29
52
  /**
30
53
  * @experimental
31
54
  * IMPORTANT: This feature is being developed for use internally and is not in a public-facing production-ready state.
@@ -42,11 +65,10 @@ export function setupRegisterAPI (agent) {
42
65
  * Also conducts certain side-effects, such as harvesting a PageView event when triggered and gathering metadata for the registered entity.
43
66
  * @param {Object} agentRef the reference to the base agent instance
44
67
  * @param {import('./register-api-types').RegisterAPIConstructor} target
45
- * @param {import('./register-api-types').RegisterAPIConstructor} [parent]
46
68
  * @returns {RegisterAPI} the api object to be returned from the register api method
47
69
  */
48
- function register (agentRef, target, parent) {
49
- warn(54, 'newrelic.register')
70
+ function register (agentRef, target) {
71
+ warnings.experimental()
50
72
 
51
73
  target ||= {}
52
74
  target.instance = generateRandomHexString(8)
@@ -54,7 +76,7 @@ function register (agentRef, target, parent) {
54
76
  target.licenseKey ||= agentRef.info.licenseKey // will inherit the license key from the container agent if not provided for brevity. A future state may dictate that we need different license keys to do different things.
55
77
  target.blocked = false
56
78
  if (typeof target.tags !== 'object' || target.tags === null || Array.isArray(target.tags)) target.tags = {}
57
- target.parent = parent || {
79
+ target.parent ??= {
58
80
  get id () { return agentRef.runtime.appMetadata.agents[0].entityGuid }, // getter because this is asyncronously set
59
81
  type: V2_TYPES.BA
60
82
  }
@@ -62,18 +84,22 @@ function register (agentRef, target, parent) {
62
84
  const timings = findScriptTimings()
63
85
 
64
86
  const attrs = {}
65
- Object.defineProperty(target, 'attributes', {
66
- get () {
67
- return {
68
- ...attrs,
69
- 'source.id': target.id,
70
- 'source.name': target.name,
71
- 'source.type': target.type,
72
- 'parent.type': target.parent?.type || V2_TYPES.BA,
73
- 'parent.id': target.parent?.id
87
+
88
+ // Only define attributes getter if it doesn't already exist
89
+ if (!Object.prototype.hasOwnProperty.call(target, 'attributes')) {
90
+ Object.defineProperty(target, 'attributes', {
91
+ get () {
92
+ return {
93
+ ...attrs,
94
+ 'source.id': target.id,
95
+ 'source.name': target.name,
96
+ 'source.type': target.type,
97
+ 'parent.type': target.parent?.type || V2_TYPES.BA,
98
+ 'parent.id': target.parent?.id
99
+ }
74
100
  }
75
- }
76
- })
101
+ })
102
+ }
77
103
 
78
104
  // Process tags object and add to attrs, excluding protected keys
79
105
  Object.entries(target.tags).forEach(([key, value]) => {
@@ -82,19 +108,11 @@ function register (agentRef, target, parent) {
82
108
  }
83
109
  })
84
110
 
85
- target.isolated ??= true
86
-
87
111
  /** @type {Function} a function that is set and reports when APIs are triggered -- warns the customer of the invalid state */
88
112
  let invalidApiResponse = () => {}
89
113
  /** @type {Array} the array of registered target APIs */
90
114
  const registeredEntities = agentRef.runtime.registeredEntities
91
115
 
92
- if (!target.isolated) {
93
- /** if we have already registered this non-isolated target, go ahead and re-use it */
94
- const sharedEntity = registeredEntities.find(({ metadata: { target: { id } } }) => id === target.id && !target.isolated)
95
- if (sharedEntity) return sharedEntity
96
- }
97
-
98
116
  /**
99
117
  * Block the API, and supply a warning function to display a message to end users
100
118
  * @param {Function} warning
@@ -109,8 +127,8 @@ function register (agentRef, target, parent) {
109
127
  }
110
128
 
111
129
  /** primary cases that can block the register API from working at init time */
112
- if (!agentRef.init.api.allow_registered_children) block(single(() => warn(55)))
113
- if (!hasValidValue(target.id) || !hasValidValue(target.name)) block(single(() => warn(48, target)))
130
+ if (!agentRef.init.api.register.enabled) block(warnings.disabled)
131
+ if (!hasValidValue(target.id) || !hasValidValue(target.name)) block(() => warnings.invalidTarget(target))
114
132
 
115
133
  /** @type {RegisterAPI} */
116
134
  const api = {
@@ -118,12 +136,11 @@ function register (agentRef, target, parent) {
118
136
  deregister: () => {
119
137
  /** note: blocking this instance will disable access for all entities sharing the instance, and will invalidate it from the v2 checks */
120
138
  reportTimings()
121
- block(single(() => warn(68)))
139
+ block(warnings.deregistered)
122
140
  },
123
141
  log: (message, options = {}) => report(log, [message, { ...options, customAttributes: { ...attrs, ...(options.customAttributes || {}) } }, agentRef], target),
124
142
  measure: (name, options = {}) => report(measure, [name, { ...options, customAttributes: { ...attrs, ...(options.customAttributes || {}) } }, agentRef], target),
125
143
  noticeError: (error, attributes = {}) => report(noticeError, [error, { ...attrs, ...attributes }, agentRef], target),
126
- register: (target = {}) => report(register, [agentRef, target], api.metadata.target),
127
144
  recordCustomEvent: (eventType, attributes = {}) => report(recordCustomEvent, [eventType, { ...attrs, ...attributes }, agentRef], target),
128
145
  setApplicationVersion: (value) => setLocalValue('application.version', value),
129
146
  setCustomAttribute: (key, value) => setLocalValue(key, value),
@@ -160,14 +177,17 @@ function register (agentRef, target, parent) {
160
177
  // only ever report the timings the first time this is called
161
178
  if (timings.reportedAt) return
162
179
  timings.reportedAt = now()
180
+ const timeToFetch = timings.fetchEnd - timings.fetchStart // fetchStart to fetchEnd
181
+ const timeToExecute = timings.scriptEnd - timings.scriptStart // scriptStart to scriptEnd
163
182
  api.recordCustomEvent('MicroFrontEndTiming', {
164
183
  assetUrl: timings.asset, // the url of the script that was registered, or undefined if it could not be determined (inline or no match)
165
184
  assetType: timings.type, // the type of asset that was associated with the timings, one of 'script', 'link' (if preloaded and found in the resource timing buffer), 'preload' (if preloaded but not found in the resource timing buffer), or "unknown" if it could not be determined
166
- timeToLoad: timings.registeredAt - timings.fetchStart, // fetchStart to registeredAt
185
+ timeAlive: timings.reportedAt - timings.registeredAt, // registeredAt to reportedAt
167
186
  timeToBeRequested: timings.fetchStart, // origin to fetchStart
168
- timeToFetch: timings.fetchEnd - timings.fetchStart, // fetchStart to fetchEnd
169
- timeToRegister: timings.registeredAt - timings.fetchEnd, // fetchEnd to registeredAt
170
- timeAlive: timings.reportedAt - timings.registeredAt // registeredAt to reportedAt
187
+ timeToExecute, // scriptStart to scriptEnd
188
+ timeToFetch, // fetchStart to fetchEnd
189
+ timeToLoad: timeToFetch + timeToExecute, // fetch time and script time together
190
+ timeToRegister: timings.registeredAt // timestamp when register() was called
171
191
  })
172
192
  }
173
193
 
@@ -184,7 +204,7 @@ function register (agentRef, target, parent) {
184
204
 
185
205
  /**
186
206
  * The reporter method that will be used to report the data to the container agent's API method. If invalid, will log a warning and not execute.
187
- * If the api.duplicate_registered_data configuration value is set to true, the data will be reported to BOTH the container and the external target
207
+ * If the api.register.duplicate_data_to_container configuration value is set to true, the data will be reported to BOTH the container and the external target
188
208
  * @param {*} methodToCall the container agent's API method to call
189
209
  * @param {*} args the arguments to supply to the container agent's API method
190
210
  * @param {string} target the target to report the data to. If undefined, will report to the container agent's target.
@@ -195,7 +215,8 @@ function register (agentRef, target, parent) {
195
215
  if (isBlocked() && methodToCall !== register) return
196
216
  /** set the timestamp before the async part of waiting for the rum response for better accuracy */
197
217
  const timestamp = now()
198
- handle(SUPPORTABILITY_METRIC_CHANNEL, [`API/register/${methodToCall.name}/called`], undefined, FEATURE_NAMES.metrics, agentRef.ee)
218
+ const methodName = METHOD_NAMES.get(methodToCall) || 'unknown'
219
+ handle(SUPPORTABILITY_METRIC_CHANNEL, [`API/register/${methodName}/called`], undefined, FEATURE_NAMES.metrics, agentRef.ee)
199
220
  try {
200
221
  return methodToCall(...args, target, timestamp) // always report to target
201
222
  } catch (err) {
@@ -1,204 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.extractUrlsFromStack = extractUrlsFromStack;
7
- exports.findScriptTimings = findScriptTimings;
8
- exports.getDeepStackTrace = getDeepStackTrace;
9
- exports.thisFile = void 0;
10
- var _runtime = require("../constants/runtime");
11
- var _now = require("../timing/now");
12
- var _cleanUrl = require("../url/clean-url");
13
- var _browserStackMatchers = require("./browser-stack-matchers");
14
- /**
15
- * Copyright 2020-2026 New Relic, Inc. All rights reserved.
16
- * SPDX-License-Identifier: Apache-2.0
17
- */
18
-
19
- /**
20
- * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
21
- */
22
-
23
- /** export for testing purposes */
24
- let thisFile = exports.thisFile = void 0;
25
- try {
26
- exports.thisFile = thisFile = extractUrlsFromStack(getDeepStackTrace())[0];
27
- } catch (err) {
28
- exports.thisFile = thisFile = extractUrlsFromStack(err)[0];
29
- }
30
-
31
- /** @type {(entry: PerformanceEntry) => boolean} - A shared function to determine if a performance entry is a valid script or link resource for evaluation */
32
- const validEntryCriteria = entry => entry.initiatorType === 'script' || ['link', 'fetch'].includes(entry.initiatorType) && entry.name.endsWith('.js');
33
-
34
- /** @type {Set<PerformanceResourceTiming>} - A set of resource timing objects that are "valid" -- see "validEntryCriteria" */
35
- const scripts = new Set();
36
- /** @type {Array<{ test: (entry: PerformanceEntry) => boolean, addedAt: number }>} an array of PerformanceObserver subscribers to check for late emissions */
37
- let poSubscribers = [];
38
- if (_runtime.globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
39
- /** We must track the script assets this way, because the performance buffer can fill up and when it does that
40
- * it stops accepting new entries (instead of dropping old entries), which means if the register API is called
41
- * after the buffer fills up we won't be able to get the script timing information from the resource timing API
42
- */
43
- const scriptObserver = new PerformanceObserver(list => {
44
- list.getEntries().forEach(entry => {
45
- if (validEntryCriteria(entry)) {
46
- if (scripts.size > 250) scripts.delete(scripts.values().next().value); // keep the set from growing indefinitely, we only need to check recent entries against the stack of the register API caller, so we can drop old entries as new ones come in
47
- scripts.add(entry);
48
- const canClear = [];
49
- poSubscribers.forEach(({
50
- test,
51
- addedAt
52
- }, idx) => {
53
- if (test(entry) || (0, _now.now)() - addedAt > 10000) canClear.push(idx); // Clear subscribers that have resolved or have been pending for more than 10 seconds
54
- });
55
- poSubscribers = poSubscribers.filter((_, idx) => !canClear.includes(idx));
56
- }
57
- });
58
- });
59
- scriptObserver.observe({
60
- type: 'resource',
61
- buffered: true
62
- });
63
- }
64
-
65
- /**
66
- * Extracts URLs from stack traces using the same logic as compute-stack-trace.js
67
- * @param {string} stack The error stack trace
68
- * @returns {string[]} Array of cleaned URLs found in the stack trace
69
- */
70
- function extractUrlsFromStack(stack) {
71
- if (!stack || typeof stack !== 'string') return [];
72
- const urls = new Set();
73
- const lines = stack.split('\n');
74
- for (const line of lines) {
75
- // Try gecko format first, then chrome
76
- const parts = line.match(_browserStackMatchers.gecko) || line.match(_browserStackMatchers.chrome) || line.match(_browserStackMatchers.chromeEval);
77
- if (parts && parts[2]) {
78
- urls.add((0, _cleanUrl.cleanURL)(parts[2]));
79
- } else {
80
- // Fallback: match URLs using a generic .js pattern (non-greedy to handle ports and query params)
81
- const fallbackMatch = line.match(/\(([^)]+\.js):\d+:\d+\)/) || line.match(/^\s+at\s+([^\s(]+\.js):\d+:\d+/);
82
- if (fallbackMatch && fallbackMatch[1]) {
83
- urls.add((0, _cleanUrl.cleanURL)(fallbackMatch[1]));
84
- }
85
- }
86
- }
87
- return [...urls];
88
- }
89
-
90
- /**
91
- * Returns a deep stack trace by temporarily increasing the stack trace limit.
92
- * @returns {Error.stack | undefined}
93
- */
94
- function getDeepStackTrace() {
95
- let stack;
96
- try {
97
- const originalStackLimit = Error.stackTraceLimit;
98
- Error.stackTraceLimit = 50;
99
- stack = new Error().stack;
100
- Error.stackTraceLimit = originalStackLimit;
101
- } catch (e) {
102
- stack = new Error().stack;
103
- }
104
- return stack;
105
- }
106
-
107
- /**
108
- * Indicates whether the provided URL matches any script preload link tags in the document.
109
- * @param {string} targetUrl The URL to match against preload tags
110
- * @returns {boolean} True if a matching preload link is found, false otherwise
111
- */
112
- function wasPreloaded(targetUrl) {
113
- if (!targetUrl || !_runtime.globalScope.document) return false;
114
- try {
115
- const linkTags = _runtime.globalScope.document.querySelectorAll('link[rel="preload"][as="script"]');
116
- for (const link of linkTags) {
117
- // 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
118
- if ((0, _cleanUrl.cleanURL)(link.href) === targetUrl) return true;
119
- }
120
- } catch (error) {
121
- // Don't let DOM parsing errors break anything
122
- }
123
- return false;
124
- }
125
-
126
- /**
127
- * 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.
128
- * @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
129
- */
130
- function findScriptTimings() {
131
- const timings = {
132
- registeredAt: (0, _now.now)(),
133
- reportedAt: undefined,
134
- fetchStart: 0,
135
- fetchEnd: 0,
136
- asset: undefined,
137
- type: 'unknown'
138
- };
139
- const stack = getDeepStackTrace();
140
- if (!stack) return timings;
141
- const navUrl = _runtime.globalScope.performance?.getEntriesByType('navigation')?.[0]?.name || '';
142
- try {
143
- const urls = extractUrlsFromStack(stack);
144
- /** if there is exactly one url, this means the MFE script is running in the same file as the agent. Otherwise, lets strip away the known agent file from any other file lines */
145
- const mfeScriptUrl = (urls.length > 1 ? urls.filter(line => !thisFile.endsWith(line) && !line.endsWith(thisFile)) : urls)[0];
146
- if (!mfeScriptUrl) return timings;
147
- if (navUrl.includes(mfeScriptUrl)) {
148
- // this means the stack is indicating that the registration came from an inline script or eval, so we won't find a matching script resource - return early with just the URL
149
- timings.asset = (0, _cleanUrl.cleanURL)(navUrl);
150
- timings.type = 'inline';
151
- return timings;
152
- }
153
-
154
- // try to find it in the static list, which updates faster than the PO. This cant be solely trusted, since the buffer can fill up and stop accepting entries,
155
- // but it can still help in cases where the check is made before the asset is emitted by the performance observer and the buffer is not full. Fallback to checking the PO
156
- // entries that have been buffered as seen in the PO callback if its not found in the static list.
157
- const match = performance.getEntriesByType('resource').find(entryMatchesMfe) || [...scripts].find(entryMatchesMfe);
158
- if (match) {
159
- setMatchedAttributes(match);
160
- } else {
161
- // We didnt find a match with the PO, nor a static lookup... check if its preloaded and if so, set basic fallbacks and try to associate with a later script entry if possible, this can happen if the preload is reported late in the PO observer callback
162
- if (wasPreloaded(mfeScriptUrl)) {
163
- timings.asset = mfeScriptUrl;
164
- timings.type = 'preload';
165
-
166
- // wait for a late PO callback... The timings object can be mutated after the fact since we return a pointer and not a cloned object
167
- poSubscribers.push({
168
- addedAt: (0, _now.now)(),
169
- test: entry => {
170
- if (entryMatchesMfe(entry)) {
171
- setMatchedAttributes(entry);
172
- return true; // return true so that we know to clear this callback from the pending list since we found our match, otherwise it will stay in the list and be called for future entries which is unnecessary after we found our match and can cause performance issues if there are a lot of future entries and pending callbacks
173
- }
174
- return false;
175
- }
176
- });
177
- }
178
- }
179
-
180
- /**
181
- * A matcher function to determine if a performance entry corresponds to the MFE script based on URL matching. It checks if either the entry URL ends with the MFE script URL or vice versa, to account for potential differences in how URLs are represented in the stack trace versus the resource timing entries.
182
- * @param {PerformanceResourceTiming} entry - The resource timing entry to compare to the MFE script
183
- * @returns {boolean} True if we think the entry matches the MFE script, false otherwise
184
- */
185
- function entryMatchesMfe(entry) {
186
- const entryUrl = (0, _cleanUrl.cleanURL)(entry.name);
187
- return entryUrl.endsWith(mfeScriptUrl) || mfeScriptUrl.endsWith(entryUrl);
188
- }
189
-
190
- /**
191
- * A helper function to set the matched timing attributes on the timings object based on a performance entry. This is called when we have identified a resource timing entry that we believe corresponds to the MFE script, and we want to extract the fetch start and end times, as well as the asset URL and type.
192
- * @param {PerformanceResourceTiming} entry - The resource timing entry to base values off of
193
- */
194
- function setMatchedAttributes(entry) {
195
- timings.fetchStart = Math.floor(entry.startTime);
196
- timings.fetchEnd = Math.floor(entry.responseEnd);
197
- timings.asset = entry.name;
198
- timings.type = entry.initiatorType;
199
- }
200
- } catch (error) {
201
- // Don't let stack parsing errors break anything
202
- }
203
- return timings;
204
- }
@@ -1,196 +0,0 @@
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 './browser-stack-matchers';
10
-
11
- /**
12
- * @typedef {import('./register-api-types').RegisterAPITimings} RegisterAPITimings
13
- */
14
-
15
- /** export for testing purposes */
16
- export let thisFile;
17
- try {
18
- thisFile = extractUrlsFromStack(getDeepStackTrace())[0];
19
- } catch (err) {
20
- thisFile = extractUrlsFromStack(err)[0];
21
- }
22
-
23
- /** @type {(entry: PerformanceEntry) => boolean} - A shared function to determine if a performance entry is a valid script or link resource for evaluation */
24
- const validEntryCriteria = entry => entry.initiatorType === 'script' || ['link', 'fetch'].includes(entry.initiatorType) && entry.name.endsWith('.js');
25
-
26
- /** @type {Set<PerformanceResourceTiming>} - A set of resource timing objects that are "valid" -- see "validEntryCriteria" */
27
- const scripts = new Set();
28
- /** @type {Array<{ test: (entry: PerformanceEntry) => boolean, addedAt: number }>} an array of PerformanceObserver subscribers to check for late emissions */
29
- let poSubscribers = [];
30
- if (globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
31
- /** We must track the script assets this way, because the performance buffer can fill up and when it does that
32
- * it stops accepting new entries (instead of dropping old entries), which means if the register API is called
33
- * after the buffer fills up we won't be able to get the script timing information from the resource timing API
34
- */
35
- const scriptObserver = new PerformanceObserver(list => {
36
- list.getEntries().forEach(entry => {
37
- if (validEntryCriteria(entry)) {
38
- if (scripts.size > 250) scripts.delete(scripts.values().next().value); // keep the set from growing indefinitely, we only need to check recent entries against the stack of the register API caller, so we can drop old entries as new ones come in
39
- scripts.add(entry);
40
- const canClear = [];
41
- poSubscribers.forEach(({
42
- test,
43
- addedAt
44
- }, idx) => {
45
- if (test(entry) || now() - addedAt > 10000) canClear.push(idx); // Clear subscribers that have resolved or have been pending for more than 10 seconds
46
- });
47
- poSubscribers = poSubscribers.filter((_, idx) => !canClear.includes(idx));
48
- }
49
- });
50
- });
51
- scriptObserver.observe({
52
- type: 'resource',
53
- buffered: true
54
- });
55
- }
56
-
57
- /**
58
- * Extracts URLs from stack traces using the same logic as compute-stack-trace.js
59
- * @param {string} stack The error stack trace
60
- * @returns {string[]} Array of cleaned URLs found in the stack trace
61
- */
62
- export function extractUrlsFromStack(stack) {
63
- if (!stack || typeof stack !== 'string') return [];
64
- const urls = new Set();
65
- const lines = stack.split('\n');
66
- for (const line of lines) {
67
- // Try gecko format first, then chrome
68
- const parts = line.match(gecko) || line.match(chrome) || line.match(chromeEval);
69
- if (parts && parts[2]) {
70
- urls.add(cleanURL(parts[2]));
71
- } else {
72
- // Fallback: match URLs using a generic .js pattern (non-greedy to handle ports and query params)
73
- const fallbackMatch = line.match(/\(([^)]+\.js):\d+:\d+\)/) || line.match(/^\s+at\s+([^\s(]+\.js):\d+:\d+/);
74
- if (fallbackMatch && fallbackMatch[1]) {
75
- urls.add(cleanURL(fallbackMatch[1]));
76
- }
77
- }
78
- }
79
- return [...urls];
80
- }
81
-
82
- /**
83
- * Returns a deep stack trace by temporarily increasing the stack trace limit.
84
- * @returns {Error.stack | undefined}
85
- */
86
- export function getDeepStackTrace() {
87
- let stack;
88
- try {
89
- const originalStackLimit = Error.stackTraceLimit;
90
- Error.stackTraceLimit = 50;
91
- stack = new Error().stack;
92
- Error.stackTraceLimit = originalStackLimit;
93
- } catch (e) {
94
- stack = new Error().stack;
95
- }
96
- return stack;
97
- }
98
-
99
- /**
100
- * Indicates whether the provided URL matches any script preload link tags in the document.
101
- * @param {string} targetUrl The URL to match against preload tags
102
- * @returns {boolean} True if a matching preload link is found, false otherwise
103
- */
104
- function wasPreloaded(targetUrl) {
105
- if (!targetUrl || !globalScope.document) return false;
106
- try {
107
- const linkTags = globalScope.document.querySelectorAll('link[rel="preload"][as="script"]');
108
- for (const link of linkTags) {
109
- // 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
110
- if (cleanURL(link.href) === targetUrl) return true;
111
- }
112
- } catch (error) {
113
- // Don't let DOM parsing errors break anything
114
- }
115
- return false;
116
- }
117
-
118
- /**
119
- * 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.
120
- * @returns {RegisterAPITimings} Object containing script fetch start and end times, and the asset URL if found
121
- */
122
- export function findScriptTimings() {
123
- const timings = {
124
- registeredAt: now(),
125
- reportedAt: undefined,
126
- fetchStart: 0,
127
- fetchEnd: 0,
128
- asset: undefined,
129
- type: 'unknown'
130
- };
131
- const stack = getDeepStackTrace();
132
- if (!stack) return timings;
133
- const navUrl = globalScope.performance?.getEntriesByType('navigation')?.[0]?.name || '';
134
- try {
135
- const urls = extractUrlsFromStack(stack);
136
- /** if there is exactly one url, this means the MFE script is running in the same file as the agent. Otherwise, lets strip away the known agent file from any other file lines */
137
- const mfeScriptUrl = (urls.length > 1 ? urls.filter(line => !thisFile.endsWith(line) && !line.endsWith(thisFile)) : urls)[0];
138
- if (!mfeScriptUrl) return timings;
139
- if (navUrl.includes(mfeScriptUrl)) {
140
- // this means the stack is indicating that the registration came from an inline script or eval, so we won't find a matching script resource - return early with just the URL
141
- timings.asset = cleanURL(navUrl);
142
- timings.type = 'inline';
143
- return timings;
144
- }
145
-
146
- // try to find it in the static list, which updates faster than the PO. This cant be solely trusted, since the buffer can fill up and stop accepting entries,
147
- // but it can still help in cases where the check is made before the asset is emitted by the performance observer and the buffer is not full. Fallback to checking the PO
148
- // entries that have been buffered as seen in the PO callback if its not found in the static list.
149
- const match = performance.getEntriesByType('resource').find(entryMatchesMfe) || [...scripts].find(entryMatchesMfe);
150
- if (match) {
151
- setMatchedAttributes(match);
152
- } else {
153
- // We didnt find a match with the PO, nor a static lookup... check if its preloaded and if so, set basic fallbacks and try to associate with a later script entry if possible, this can happen if the preload is reported late in the PO observer callback
154
- if (wasPreloaded(mfeScriptUrl)) {
155
- timings.asset = mfeScriptUrl;
156
- timings.type = 'preload';
157
-
158
- // wait for a late PO callback... The timings object can be mutated after the fact since we return a pointer and not a cloned object
159
- poSubscribers.push({
160
- addedAt: now(),
161
- test: entry => {
162
- if (entryMatchesMfe(entry)) {
163
- setMatchedAttributes(entry);
164
- return true; // return true so that we know to clear this callback from the pending list since we found our match, otherwise it will stay in the list and be called for future entries which is unnecessary after we found our match and can cause performance issues if there are a lot of future entries and pending callbacks
165
- }
166
- return false;
167
- }
168
- });
169
- }
170
- }
171
-
172
- /**
173
- * A matcher function to determine if a performance entry corresponds to the MFE script based on URL matching. It checks if either the entry URL ends with the MFE script URL or vice versa, to account for potential differences in how URLs are represented in the stack trace versus the resource timing entries.
174
- * @param {PerformanceResourceTiming} entry - The resource timing entry to compare to the MFE script
175
- * @returns {boolean} True if we think the entry matches the MFE script, false otherwise
176
- */
177
- function entryMatchesMfe(entry) {
178
- const entryUrl = cleanURL(entry.name);
179
- return entryUrl.endsWith(mfeScriptUrl) || mfeScriptUrl.endsWith(entryUrl);
180
- }
181
-
182
- /**
183
- * A helper function to set the matched timing attributes on the timings object based on a performance entry. This is called when we have identified a resource timing entry that we believe corresponds to the MFE script, and we want to extract the fetch start and end times, as well as the asset URL and type.
184
- * @param {PerformanceResourceTiming} entry - The resource timing entry to base values off of
185
- */
186
- function setMatchedAttributes(entry) {
187
- timings.fetchStart = Math.floor(entry.startTime);
188
- timings.fetchEnd = Math.floor(entry.responseEnd);
189
- timings.asset = entry.name;
190
- timings.type = entry.initiatorType;
191
- }
192
- } catch (error) {
193
- // Don't let stack parsing errors break anything
194
- }
195
- return timings;
196
- }
@@ -1 +0,0 @@
1
- {"version":3,"file":"script-tracker.d.ts","sourceRoot":"","sources":["../../../../src/common/util/script-tracker.js"],"names":[],"mappings":"AAmDA;;;;GAIG;AACH,4CAHW,MAAM,GACJ,MAAM,EAAE,CAsBpB;AAED;;;GAGG;AACH,qCAFa,KAAK,CAAC,KAAK,GAAG,SAAS,CAanC;AAsBD;;;GAGG;AACH,qCAFa,kBAAkB,CAuE9B;AAlLD;;GAEG;AAEH,kCAAkC;AAClC,yBAAmB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"v2.d.ts","sourceRoot":"","sources":["../../../../src/common/util/v2.js"],"names":[],"mappings":"AAkBA;;;;;GAKG;AACH,+CAJW,MAAM,GAAC,MAAM,YACb,GAAC,GACC,OAAO,oCAAoC,EAAE,yBAAyB,EAAE,CAMpF;AAED;;;;;GAKG;AACH,2DAJW,MAAM,YACN,GAAC,GACC,OAAO,oCAAoC,EAAE,yBAAyB,EAAE,CAMpF;AAED;;;;;;;GAOG;AACH,+CAJW,MAAM,sBACN,iBAAiB,GACf,MAAM,CAclB;AAED;;;;;;;GAOG;AACH,yDAJW,OAAO,oCAAoC,EAAE,yBAAyB,qBACtE,GAAC,GACC,MAAM,CAKlB;AAED;;;;;;GAMG;AACH,wCAJW,OAAO,oCAAoC,EAAE,yBAAyB,qBACtE,GAAC,GACC,OAAO,CAInB;AAED;;;;GAIG;AACH,oDAHW,GAAC,SAkBX;;;;uBApGS,MAAM"}