@newrelic/browser-agent 1.323.0 → 1.324.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 (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/common/constants/agent-constants.js +5 -1
  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/util/attribute-size.js +5 -2
  6. package/dist/cjs/common/v2/manifest.js +1 -1
  7. package/dist/cjs/common/v2/script-tracker-types.js +3 -0
  8. package/dist/cjs/common/v2/script-tracker.js +55 -24
  9. package/dist/cjs/features/ajax/aggregate/index.js +17 -4
  10. package/dist/cjs/features/jserrors/aggregate/index.js +22 -0
  11. package/dist/cjs/features/utils/aggregate-base.js +10 -1
  12. package/dist/cjs/interfaces/registered-iframe-entity.js +4 -1
  13. package/dist/esm/common/constants/agent-constants.js +4 -0
  14. package/dist/esm/common/constants/env.cdn.js +1 -1
  15. package/dist/esm/common/constants/env.npm.js +1 -1
  16. package/dist/esm/common/util/attribute-size.js +5 -2
  17. package/dist/esm/common/v2/manifest.js +1 -1
  18. package/dist/esm/common/v2/script-tracker-types.js +3 -0
  19. package/dist/esm/common/v2/script-tracker.js +55 -24
  20. package/dist/esm/features/ajax/aggregate/index.js +18 -5
  21. package/dist/esm/features/jserrors/aggregate/index.js +22 -0
  22. package/dist/esm/features/utils/aggregate-base.js +11 -2
  23. package/dist/esm/interfaces/registered-iframe-entity.js +4 -1
  24. package/dist/types/common/constants/agent-constants.d.ts +4 -0
  25. package/dist/types/common/constants/agent-constants.d.ts.map +1 -1
  26. package/dist/types/common/util/attribute-size.d.ts.map +1 -1
  27. package/dist/types/common/v2/manifest.d.ts +1 -1
  28. package/dist/types/common/v2/script-tracker-types.d.ts +6 -0
  29. package/dist/types/common/v2/script-tracker-types.d.ts.map +1 -1
  30. package/dist/types/common/v2/script-tracker.d.ts +3 -1
  31. package/dist/types/common/v2/script-tracker.d.ts.map +1 -1
  32. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  33. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  34. package/dist/types/features/utils/aggregate-base.d.ts +5 -1
  35. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  36. package/dist/types/interfaces/registered-iframe-entity.d.ts.map +1 -1
  37. package/package.json +4 -3
  38. package/src/common/constants/agent-constants.js +4 -0
  39. package/src/common/util/attribute-size.js +5 -2
  40. package/src/common/v2/manifest.js +1 -1
  41. package/src/common/v2/script-tracker-types.js +3 -0
  42. package/src/common/v2/script-tracker.js +51 -24
  43. package/src/features/ajax/aggregate/index.js +15 -2
  44. package/src/features/jserrors/aggregate/index.js +22 -0
  45. package/src/features/utils/aggregate-base.js +11 -2
  46. package/src/interfaces/registered-iframe-entity.js +4 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.324.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.323.0...v1.324.0) (2026-10-12)
7
+
8
+
9
+ ### Features
10
+
11
+ * Enable distributed tracing for registered Micro Frontends ([#1871](https://github.com/newrelic/newrelic-browser-agent/issues/1871)) ([b4b2e96](https://github.com/newrelic/newrelic-browser-agent/commit/b4b2e9686fc454642454e1cf5804507f1449d495))
12
+ * Improve script matching in manifest ([#1879](https://github.com/newrelic/newrelic-browser-agent/issues/1879)) ([5b7d6fa](https://github.com/newrelic/newrelic-browser-agent/commit/5b7d6fa2435967156cb520a0b73ac1934638da74))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * find iframe entity script timings at registration ([#1891](https://github.com/newrelic/newrelic-browser-agent/issues/1891)) ([13f9fbe](https://github.com/newrelic/newrelic-browser-agent/commit/13f9fbe877ef1471200bb1460c1cb0158c302ad7))
18
+ * **jserrors:** reset stack hash memory on session reset ([#1878](https://github.com/newrelic/newrelic-browser-agent/issues/1878)) ([b0d01bf](https://github.com/newrelic/newrelic-browser-agent/commit/b0d01bf582782d2176d1715aaab290019106eef3))
19
+ * prevent runaway log harvests from inflated attribute size ([#1897](https://github.com/newrelic/newrelic-browser-agent/issues/1897)) ([1803e4c](https://github.com/newrelic/newrelic-browser-agent/commit/1803e4c1800e12c0486625e26b39e2a00146ab64))
20
+
6
21
  ## [1.323.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.322.0...v1.323.0) (2026-09-30)
7
22
 
8
23
 
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.SUPPORTS_REGISTERED_ENTITIES = exports.SESSION_ERROR = exports.MAX_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = exports.DEFAULT_KEY = void 0;
6
+ exports.SUPPORTS_REGISTERED_ENTITIES = exports.SESSION_ERROR = exports.MIN_EARLY_HARVEST_INTERVAL = exports.MAX_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = exports.EARLY_HARVEST_BYPASS_SIZE = exports.DEFAULT_KEY = void 0;
7
7
  var _features = require("../../loaders/features/features");
8
8
  /**
9
9
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
@@ -12,6 +12,10 @@ var _features = require("../../loaders/features/features");
12
12
 
13
13
  const IDEAL_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = 16000;
14
14
  const MAX_PAYLOAD_SIZE = exports.MAX_PAYLOAD_SIZE = 1000000;
15
+ /** The minimum time (ms) between early harvests of a single feature. Data that arrives inside this window stays buffered for the next harvest. */
16
+ const MIN_EARLY_HARVEST_INTERVAL = exports.MIN_EARLY_HARVEST_INTERVAL = 500;
17
+ /** The estimated payload size at which a feature may harvest early even inside the minimum interval, so that a flood of data cannot fill the buffer to MAX_PAYLOAD_SIZE (and start dropping events) while waiting. */
18
+ const EARLY_HARVEST_BYPASS_SIZE = exports.EARLY_HARVEST_BYPASS_SIZE = MAX_PAYLOAD_SIZE / 2;
15
19
  const DEFAULT_KEY = exports.DEFAULT_KEY = 'NR_CONTAINER_AGENT';
16
20
  const SESSION_ERROR = exports.SESSION_ERROR = 'SESSION_ERROR';
17
21
  const SUPPORTS_REGISTERED_ENTITIES = exports.SUPPORTS_REGISTERED_ENTITIES = {
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.323.0";
20
+ const VERSION = exports.VERSION = "1.324.0-rc.1";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.323.0";
20
+ const VERSION = exports.VERSION = "1.324.0-rc.1";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.trackObjectAttributeSize = trackObjectAttributeSize;
7
7
  var _stringify = require("./stringify");
8
8
  /**
9
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
9
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
10
10
  * SPDX-License-Identifier: Apache-2.0
11
11
  */
12
12
 
@@ -18,12 +18,15 @@ function trackObjectAttributeSize(parent, object) {
18
18
  // proxy attribute to calculate its size when changed
19
19
  parent[object] = new Proxy(originalAttribute, {
20
20
  set(target, prop, value) {
21
+ /* An overwrite replaces the existing entry, so its previous size must be removed first. Otherwise repeated sets of the same key inflate the count far beyond the real size. */
22
+ if (Object.prototype.hasOwnProperty.call(target, prop)) output.bytes -= prop.length + (0, _stringify.stringify)(target[prop]).length;
21
23
  output.bytes += prop.length + (0, _stringify.stringify)(value).length;
22
24
  target[prop] = value;
23
25
  return true;
24
26
  },
25
27
  deleteProperty(target, prop) {
26
- output.bytes -= prop.length + (0, _stringify.stringify)(target[prop]).length;
28
+ /* Deleting a key that was never set removes nothing, so the count must not change. */
29
+ if (Object.prototype.hasOwnProperty.call(target, prop)) output.bytes -= prop.length + (0, _stringify.stringify)(target[prop]).length;
27
30
  return delete target[prop];
28
31
  }
29
32
  });
@@ -15,7 +15,7 @@ var _invoke = require("../util/invoke");
15
15
 
16
16
  /**
17
17
  * @typedef {Object} AssetFile
18
- * @property {string|RegExp} matcher - the path/path-fragment string, or RegExp, used to match a resolved URL against this asset
18
+ * @property {string|RegExp} matcher - the path/path-fragment string, or RegExp, used to match a resolved URL against this asset. A matcher may match many resources (e.g. a RegExp for hashed chunks); every matching resource contributes to the MFE's `totalWeight`/`renderBlocking` and, per `timingMethod`, its timing window
19
19
  * @property {'script'|'asset'} [type] - optional override for script-capability inference. When omitted, script-capability is inferred from a
20
20
  * `.js` suffix on a string `matcher` (a RegExp `matcher` is never inferred as a script). Supply `type: 'script'` to explicitly flag an entry
21
21
  * as a script regardless of its matcher shape/suffix (e.g. an extensionless URL, or a RegExp targeting a script path), or `type: 'asset'` to
@@ -20,6 +20,9 @@ exports.default = void 0;
20
20
  * `script-tracker.js`). Cleaned URLs already folded into `timings.totalWeight`/`timings.renderBlocking` by
21
21
  * `applyResourceWeight`, so the same underlying resource (e.g. a manifest asset that's also the .register calling
22
22
  * script itself) is never counted twice.
23
+ * @property {Set<string>} widenedAssetUrls - Always present. Cleaned URLs of manifest assets whose entry has already
24
+ * widened `fetchStart`/`fetchEnd`/the script window, so a duplicate load of the same URL (typically cache-served,
25
+ * with a later `responseEnd`) can't stretch the window a second time.
23
26
  * @property {RecordManifestScriptWindowFn} recordManifestScriptWindow - Always present (seeded by
24
27
  * `getOrCreateInternals`, see `script-tracker.js`). `findScriptTimings` overrides the seeded default with one that
25
28
  * folds into the live `scriptStart`/`scriptEnd` getters instead.
@@ -64,6 +64,7 @@ function getOrCreateInternals(timings) {
64
64
  if (!internals) {
65
65
  internals = {
66
66
  weighedAssetUrls: new Set(),
67
+ widenedAssetUrls: new Set(),
67
68
  recordManifestScriptWindow: (start, end) => {
68
69
  if (start) timings.scriptStart = timings.scriptStart > 0 ? Math.min(timings.scriptStart, start) : start;
69
70
  if (end) timings.scriptEnd = timings.scriptEnd > 0 ? Math.max(timings.scriptEnd, end) : end;
@@ -112,6 +113,9 @@ if (_runtime.globalScope.MutationObserver && _runtime.globalScope.document) {
112
113
  if (node.nodeName === 'SCRIPT' && node.src) {
113
114
  const cleanedSrc = (0, _cleanUrl.cleanURL)(node.src);
114
115
  const correlation = getOrCreateCorrelation(cleanedSrc);
116
+ /* The first node wins, same as the performance entry below. Skipping here also means no load/error listeners
117
+ are attached for duplicate references. */
118
+ if (correlation.dom.value) return;
115
119
  correlation.dom.start = (0, _now.now)();
116
120
  correlation.dom.value = node;
117
121
  const setEnd = () => {
@@ -139,9 +143,13 @@ if (_runtime.globalScope.PerformanceObserver?.supportedEntryTypes.includes('reso
139
143
  if (validEntryCriteria(entry)) {
140
144
  const entryUrl = (0, _cleanUrl.cleanURL)(entry.name);
141
145
  const correlation = getOrCreateCorrelation(entryUrl);
142
- correlation.performance.start = Math.floor(entry.startTime);
143
- correlation.performance.end = Math.floor(entry.responseEnd);
144
- correlation.performance.value = entry;
146
+ /* The first entry wins. A script referenced more than once is typically re-served from the browser cache, so a
147
+ later entry for the same URL would overwrite the real network timing with a near-instant one. */
148
+ if (!correlation.performance.value) {
149
+ correlation.performance.start = Math.floor(entry.startTime);
150
+ correlation.performance.end = Math.floor(entry.responseEnd);
151
+ correlation.performance.value = entry;
152
+ }
145
153
  }
146
154
 
147
155
  // Late-resolution subscribers can be for any asset type (not just scripts), so every entry is checked here,
@@ -295,7 +303,8 @@ function subscribeToLatePerformanceEntry(timings, mfeScriptUrl) {
295
303
 
296
304
  /**
297
305
  * Applies one manifest asset's performance entry to a timings object: weight/renderBlocking always accumulate;
298
- * fetchStart/fetchEnd and scriptStart/scriptEnd widen (never shrink) only when `timingMethod` calls for it; asset/
306
+ * fetchStart/fetchEnd and scriptStart/scriptEnd widen (never shrink) only when `timingMethod` calls for it, and only for
307
+ * the first entry seen per URL; asset/
299
308
  * type get anchored to the first script asset seen to resolve.
300
309
  * @param {RegisterAPITimings} timings
301
310
  * @param {PerformanceResourceTiming} entry
@@ -308,6 +317,14 @@ function applyManifestEntry(timings, entry, asset, entryState, timingMethod) {
308
317
  applyResourceWeight(timings, entry);
309
318
  if (timingMethod !== 'scripts' && timingMethod !== 'all') return; // no timing-widening effect at the 'entry' default/unset
310
319
 
320
+ /* Only the first entry per URL widens timing. A resource referenced more than once is typically re-served from the
321
+ browser cache with a later responseEnd, which would otherwise stretch fetchEnd out to the duplicate's load. */
322
+ const {
323
+ widenedAssetUrls
324
+ } = getOrCreateInternals(timings);
325
+ const url = (0, _cleanUrl.cleanURL)(entry.name);
326
+ if (widenedAssetUrls.has(url)) return;
327
+ widenedAssetUrls.add(url);
311
328
  const widensAllAssets = timingMethod === 'all';
312
329
  // Under 'scripts', only script assets widen the fetch window; under 'all', every matched asset does.
313
330
  if (widensAllAssets || asset.isScript) {
@@ -349,33 +366,45 @@ function applyManifestEntry(timings, entry, asset, entryState, timingMethod) {
349
366
  }
350
367
 
351
368
  /**
352
- * Subscribes to late resource timing emissions for manifest assets not yet resolved against the buffered entries.
353
- * Reuses the shared page-wide scriptObserver/poSubscribers mechanism (one PerformanceObserver for all MFEs, not
354
- * one per MFE) and, unlike that observer's own correlation bookkeeping, checks every resource entry -- not just
355
- * script-like ones -- so lazy-loaded images/fonts/stylesheets resolve too.
369
+ * Finds the manifest asset a resource entry should be credited to. An entry matching several assets is applied once;
370
+ * a script asset is preferred so an overlapping non-script matcher can't cause 'scripts' mode to skip a script entry.
371
+ * @param {import('./manifest').ParsedManifestAsset[]} assets
372
+ * @param {PerformanceResourceTiming} entry
373
+ * @returns {import('./manifest').ParsedManifestAsset|undefined}
374
+ */
375
+ function findMatchingManifestAsset(assets, entry) {
376
+ const matches = assets.filter(asset => asset.test(entry.name));
377
+ return matches.find(asset => asset.isScript) || matches[0];
378
+ }
379
+
380
+ /**
381
+ * Subscribes to late resource timing emissions for manifest assets. Reuses the shared page-wide
382
+ * scriptObserver/poSubscribers mechanism (one PerformanceObserver for all MFEs, not one per MFE) and, unlike that
383
+ * observer's own correlation bookkeeping, checks every resource entry -- not just script-like ones -- so lazy-loaded
384
+ * images/fonts/stylesheets resolve too. A matcher can legitimately match many resources (e.g. a hashed-chunk RegExp),
385
+ * so this subscriber never reports itself as finished; it is cleared by the shared observer's expiry instead.
356
386
  * @param {RegisterAPITimings} timings
357
- * @param {Set<import('./manifest').ParsedManifestAsset>} pending - manifest assets still unresolved
387
+ * @param {import('./manifest').ParsedManifestAsset[]} assets - every manifest asset to match late entries against
358
388
  * @param {{ resolved: boolean }} entryState - shared "first script asset wins" guard for a single `applyManifestTimings` call
359
389
  * @param {'entry'|'scripts'|'all'} [timingMethod] - forwarded to `applyManifestEntry` for each late-resolving asset
360
390
  */
361
- function subscribeToLateManifestEntries(timings, pending, entryState, timingMethod) {
391
+ function subscribeToLateManifestEntries(timings, assets, entryState, timingMethod) {
362
392
  if (!_runtime.globalScope.PerformanceObserver?.supportedEntryTypes?.includes('resource')) return;
363
393
  poSubscribers.push({
364
394
  addedAt: (0, _now.now)(),
365
395
  test: entry => {
366
- const matched = [...pending].find(asset => asset.test(entry.name));
367
- if (matched) {
368
- applyManifestEntry(timings, entry, matched, entryState, timingMethod);
369
- pending.delete(matched);
370
- }
371
- return pending.size === 0;
396
+ const matched = findMatchingManifestAsset(assets, entry);
397
+ if (matched) applyManifestEntry(timings, entry, matched, entryState, timingMethod);
398
+ return false;
372
399
  }
373
400
  });
374
401
  }
375
402
 
376
403
  /**
377
404
  * Applies a registered MFE's manifest to a timings object (already populated by `findScriptTimings`). No-op if no
378
- * manifest is present. Weight/renderBlocking always accumulate from every detected manifest asset; timing widening
405
+ * manifest is present. Every resource entry matching any manifest asset contributes -- an asset is not consumed by its
406
+ * first match, so a broad matcher (e.g. a RegExp for hashed chunks) credits all of the chunks it matches.
407
+ * Weight/renderBlocking always accumulate from every detected manifest asset; timing widening
379
408
  * (fetchStart/fetchEnd/scriptStart/scriptEnd/asset anchor) is opt-in via `timingMethod` -- see `applyManifestEntry`.
380
409
  * @param {RegisterAPITimings} timings - the timings object to widen in place
381
410
  * @param {RegisterAPITarget} target - the registered MFE target, which may carry a parsed `manifest`
@@ -386,16 +415,18 @@ function applyManifestTimings(timings, target) {
386
415
  const entryState = {
387
416
  resolved: false
388
417
  };
389
- const pending = new Set(parsedManifest.assets);
418
+ const {
419
+ assets
420
+ } = parsedManifest;
390
421
  const resourceEntries = _runtime.globalScope.performance?.getEntriesByType('resource') || [];
391
422
  resourceEntries.forEach(entry => {
392
- const matched = [...pending].find(asset => asset.test(entry.name));
393
- if (matched) {
394
- applyManifestEntry(timings, entry, matched, entryState, target.timingMethod);
395
- pending.delete(matched);
396
- }
423
+ const matched = findMatchingManifestAsset(assets, entry);
424
+ if (matched) applyManifestEntry(timings, entry, matched, entryState, target.timingMethod);
397
425
  });
398
- if (pending.size) subscribeToLateManifestEntries(timings, pending, entryState, target.timingMethod);
426
+
427
+ // Always subscribe: assets may still match resources that load after this point, and (unlike an exact matcher) a
428
+ // wildcard can't know its last match has been seen.
429
+ subscribeToLateManifestEntries(timings, assets, entryState, target.timingMethod);
399
430
  }
400
431
 
401
432
  /**
@@ -133,10 +133,23 @@ class Aggregate extends _aggregateBase.AggregateBase {
133
133
  ...event,
134
134
  targetAttributes: (0, _utils.getVersion2Attributes)(target, this)
135
135
  });
136
- if ((0, _utils.shouldDuplicate)(target, this)) this.reportContainerEvent({
137
- ...event,
138
- targetAttributes: (0, _utils.getVersion2DuplicationAttributes)(target, this)
139
- }, ctx);
136
+ if ((0, _utils.shouldDuplicate)(target, this)) {
137
+ const duplicateEvent = {
138
+ ...event,
139
+ targetAttributes: (0, _utils.getVersion2DuplicationAttributes)(target, this)
140
+ };
141
+ /** the container-duplicate copy represents the same single real HTTP call as the MFE event above, so it
142
+ * needs its own independent trace/span identity -- otherwise the two would collide on the same
143
+ * spanId/traceId, and BELC could not create two separate Spans/Traces from them. Only the id namespace is
144
+ * independent; spanTimestamp is intentionally left as-is (event.spanTimestamp), since it still reflects the
145
+ * real call's actual timing. When `event.spanId` was never set (e.g. distributed tracing is disabled), there
146
+ * is no span identity to duplicate either -- both copies correctly end up with no spanId/traceId. */
147
+ if (event.spanId) {
148
+ duplicateEvent.spanId = (0, _uniqueId.generateSpanId)();
149
+ duplicateEvent.traceId = (0, _uniqueId.generateTraceId)();
150
+ }
151
+ this.reportContainerEvent(duplicateEvent, ctx);
152
+ }
140
153
  } else {
141
154
  this.reportContainerEvent(event, ctx);
142
155
  }
@@ -22,6 +22,7 @@ var _utils = require("../../../common/v2/utils");
22
22
  var _causeString = require("./cause-string");
23
23
  var _shortCircuit = require("../../../common/util/short-circuit");
24
24
  var _events = require("../../../common/constants/events");
25
+ var _constants2 = require("../../../common/session/constants");
25
26
  /**
26
27
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
27
28
  * SPDX-License-Identifier: Apache-2.0
@@ -50,6 +51,27 @@ class Aggregate extends _aggregateBase.AggregateBase {
50
51
  (0, _registerHandler.registerHandler)('ierr', this.processError.bind(this), this.featureName, this.ee);
51
52
  (0, _registerHandler.registerHandler)('returnJserror', (jsErrorEvent, softNavAttrs) => this.#storeJserrorForHarvest(jsErrorEvent, softNavAttrs), this.featureName, this.ee);
52
53
 
54
+ /*
55
+ * The backend only remembers a stack trace for as long as the session that first reported it.
56
+ * The SessionEntity emits SESSION_EVENTS.RESET whenever the session expires (max duration or
57
+ * inactivity) or is otherwise reset, which is the same boundary the backend uses to forget
58
+ * previously-seen stack hashes. Forget them here too, so the next occurrence of an
59
+ * already-seen error resends its full stack_trace once, instead of a browser_stack_hash the
60
+ * backend can no longer resolve.
61
+ */
62
+ const forgetReportedStacks = () => {
63
+ this.stackReported = {};
64
+ this.observedAt = {};
65
+ };
66
+ let currentSessionId = this.agentRef.runtime.session?.state?.value;
67
+ this.ee.on(_constants2.SESSION_EVENTS.RESET, forgetReportedStacks);
68
+ this.ee.on(_constants2.SESSION_EVENTS.UPDATE, (type, data) => {
69
+ if (!data?.value) return;
70
+ /* A reset in another tab arrived as a cross-tab update carrying a new session id. */
71
+ if (type === _constants2.SESSION_EVENT_TYPES.CROSS_TAB && currentSessionId && data.value !== currentSessionId) forgetReportedStacks();
72
+ currentSessionId = data.value; // also tracks same-tab writes, so a local reset's new id is picked up
73
+ });
74
+
53
75
  // 0 == off, 1 == on
54
76
  this.waitForFlags(['err']).then(([errFlag]) => {
55
77
  if (errFlag) {
@@ -13,6 +13,7 @@ var _constants = require("../metrics/constants");
13
13
  var _eventAggregator = require("../../common/aggregate/event-aggregator");
14
14
  var _agentConstants = require("../../common/constants/agent-constants");
15
15
  var _console = require("../../common/util/console");
16
+ var _now = require("../../common/timing/now");
16
17
  /**
17
18
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
18
19
  * SPDX-License-Identifier: Apache-2.0
@@ -33,6 +34,8 @@ class AggregateBase extends _featureBase.FeatureBase {
33
34
  this.canHarvestEarly = true;
34
35
  /** @type {Boolean} indicates if the feature is actively in a retry deferral period */
35
36
  this.isRetrying = false;
37
+ /** @type {number|undefined} the timestamp (ms, relative to page start) of the last early harvest triggered by this feature, if any */
38
+ this.lastEarlyHarvestAt = undefined;
36
39
  this.harvestOpts = {}; // features aggregate classes can define custom opts for when their harvest is called
37
40
 
38
41
  this.#setupEventStore();
@@ -88,13 +91,19 @@ class AggregateBase extends _featureBase.FeatureBase {
88
91
 
89
92
  /**
90
93
  * Evaluates whether a harvest should be made early by estimating the size of the current payload. Currently, this only happens if the event storage is EventBuffer, since that triggers this method directly.
91
- * If conditions are met, a new harvest will be triggered immediately.
94
+ * If conditions are met, a new harvest will be triggered immediately, unless this feature already harvested early within the last `MIN_EARLY_HARVEST_INTERVAL` ms.
95
+ * Data held back by that limit stays buffered until the next early, interval, or final harvest. Once the estimated payload reaches `EARLY_HARVEST_BYPASS_SIZE`
96
+ * the limit no longer applies, so a flood of data cannot fill the buffer to its maximum size, where new events would be dropped, while waiting.
92
97
  * @returns void
93
98
  */
94
99
  decideEarlyHarvest() {
95
100
  if (!this.canHarvestEarly || this.blocked || this.isRetrying) return;
96
101
  const estimatedSize = this.events.byteSize() + (this.customAttributesAreSeparate ? this.agentRef.runtime.jsAttributesMetadata.bytes : 0);
97
102
  if (estimatedSize > _agentConstants.IDEAL_PAYLOAD_SIZE) {
103
+ const currentTime = (0, _now.now)();
104
+ /* Bounds the request rate when a page produces data in a flood, regardless of how fast or whether requests complete. Large payloads skip the limit to avoid dropping data. */
105
+ if (estimatedSize < _agentConstants.EARLY_HARVEST_BYPASS_SIZE && this.lastEarlyHarvestAt !== undefined && currentTime - this.lastEarlyHarvestAt < _agentConstants.MIN_EARLY_HARVEST_INTERVAL) return;
106
+ this.lastEarlyHarvestAt = currentTime;
98
107
  this.agentRef.runtime.harvester.triggerHarvestFor(this);
99
108
  this.reportSupportabilityMetric("".concat(this.featureName, "/Harvest/Early/Seen"), estimatedSize);
100
109
  }
@@ -123,10 +123,13 @@ class RegisteredIframeEntity {
123
123
  return;
124
124
  }
125
125
 
126
+ /* Script timings are found by parsing the call stack for the script that called register(), so this has to run
127
+ synchronously here. By the time the registration promise resolves, that caller is no longer on the stack. */
128
+ const timings = (0, _scriptTracker.findScriptTimings)(opts);
129
+
126
130
  // Store the registration promise so other methods can wait for it
127
131
  this.#registrationPromise = this.#register(opts);
128
132
  this.#registrationPromise.then(() => {
129
- const timings = (0, _scriptTracker.findScriptTimings)();
130
133
  // Send initial timing values, skipping ones still at their "not yet known" default
131
134
  // (e.g. reportedAt: undefined, fetchStart: 0) to avoid pointless postMessage calls
132
135
  for (const [key, value] of Object.entries(timings)) {
@@ -5,6 +5,10 @@
5
5
  import { FEATURE_NAMES } from '../../loaders/features/features';
6
6
  export const IDEAL_PAYLOAD_SIZE = 16000;
7
7
  export const MAX_PAYLOAD_SIZE = 1000000;
8
+ /** The minimum time (ms) between early harvests of a single feature. Data that arrives inside this window stays buffered for the next harvest. */
9
+ export const MIN_EARLY_HARVEST_INTERVAL = 500;
10
+ /** The estimated payload size at which a feature may harvest early even inside the minimum interval, so that a flood of data cannot fill the buffer to MAX_PAYLOAD_SIZE (and start dropping events) while waiting. */
11
+ export const EARLY_HARVEST_BYPASS_SIZE = MAX_PAYLOAD_SIZE / 2;
8
12
  export const DEFAULT_KEY = 'NR_CONTAINER_AGENT';
9
13
  export const SESSION_ERROR = 'SESSION_ERROR';
10
14
  export const SUPPORTS_REGISTERED_ENTITIES = {
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.323.0";
14
+ export const VERSION = "1.324.0-rc.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.323.0";
14
+ export const VERSION = "1.324.0-rc.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { stringify } from './stringify';
@@ -11,12 +11,15 @@ export function trackObjectAttributeSize(parent, object) {
11
11
  // proxy attribute to calculate its size when changed
12
12
  parent[object] = new Proxy(originalAttribute, {
13
13
  set(target, prop, value) {
14
+ /* An overwrite replaces the existing entry, so its previous size must be removed first. Otherwise repeated sets of the same key inflate the count far beyond the real size. */
15
+ if (Object.prototype.hasOwnProperty.call(target, prop)) output.bytes -= prop.length + stringify(target[prop]).length;
14
16
  output.bytes += prop.length + stringify(value).length;
15
17
  target[prop] = value;
16
18
  return true;
17
19
  },
18
20
  deleteProperty(target, prop) {
19
- output.bytes -= prop.length + stringify(target[prop]).length;
21
+ /* Deleting a key that was never set removes nothing, so the count must not change. */
22
+ if (Object.prototype.hasOwnProperty.call(target, prop)) output.bytes -= prop.length + stringify(target[prop]).length;
20
23
  return delete target[prop];
21
24
  }
22
25
  });
@@ -9,7 +9,7 @@ import { single } from '../util/invoke';
9
9
 
10
10
  /**
11
11
  * @typedef {Object} AssetFile
12
- * @property {string|RegExp} matcher - the path/path-fragment string, or RegExp, used to match a resolved URL against this asset
12
+ * @property {string|RegExp} matcher - the path/path-fragment string, or RegExp, used to match a resolved URL against this asset. A matcher may match many resources (e.g. a RegExp for hashed chunks); every matching resource contributes to the MFE's `totalWeight`/`renderBlocking` and, per `timingMethod`, its timing window
13
13
  * @property {'script'|'asset'} [type] - optional override for script-capability inference. When omitted, script-capability is inferred from a
14
14
  * `.js` suffix on a string `matcher` (a RegExp `matcher` is never inferred as a script). Supply `type: 'script'` to explicitly flag an entry
15
15
  * as a script regardless of its matcher shape/suffix (e.g. an extensionless URL, or a RegExp targeting a script path), or `type: 'asset'` to
@@ -16,6 +16,9 @@
16
16
  * `script-tracker.js`). Cleaned URLs already folded into `timings.totalWeight`/`timings.renderBlocking` by
17
17
  * `applyResourceWeight`, so the same underlying resource (e.g. a manifest asset that's also the .register calling
18
18
  * script itself) is never counted twice.
19
+ * @property {Set<string>} widenedAssetUrls - Always present. Cleaned URLs of manifest assets whose entry has already
20
+ * widened `fetchStart`/`fetchEnd`/the script window, so a duplicate load of the same URL (typically cache-served,
21
+ * with a later `responseEnd`) can't stretch the window a second time.
19
22
  * @property {RecordManifestScriptWindowFn} recordManifestScriptWindow - Always present (seeded by
20
23
  * `getOrCreateInternals`, see `script-tracker.js`). `findScriptTimings` overrides the seeded default with one that
21
24
  * folds into the live `scriptStart`/`scriptEnd` getters instead.
@@ -54,6 +54,7 @@ function getOrCreateInternals(timings) {
54
54
  if (!internals) {
55
55
  internals = {
56
56
  weighedAssetUrls: new Set(),
57
+ widenedAssetUrls: new Set(),
57
58
  recordManifestScriptWindow: (start, end) => {
58
59
  if (start) timings.scriptStart = timings.scriptStart > 0 ? Math.min(timings.scriptStart, start) : start;
59
60
  if (end) timings.scriptEnd = timings.scriptEnd > 0 ? Math.max(timings.scriptEnd, end) : end;
@@ -102,6 +103,9 @@ if (globalScope.MutationObserver && globalScope.document) {
102
103
  if (node.nodeName === 'SCRIPT' && node.src) {
103
104
  const cleanedSrc = cleanURL(node.src);
104
105
  const correlation = getOrCreateCorrelation(cleanedSrc);
106
+ /* The first node wins, same as the performance entry below. Skipping here also means no load/error listeners
107
+ are attached for duplicate references. */
108
+ if (correlation.dom.value) return;
105
109
  correlation.dom.start = now();
106
110
  correlation.dom.value = node;
107
111
  const setEnd = () => {
@@ -129,9 +133,13 @@ if (globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
129
133
  if (validEntryCriteria(entry)) {
130
134
  const entryUrl = cleanURL(entry.name);
131
135
  const correlation = getOrCreateCorrelation(entryUrl);
132
- correlation.performance.start = Math.floor(entry.startTime);
133
- correlation.performance.end = Math.floor(entry.responseEnd);
134
- correlation.performance.value = entry;
136
+ /* The first entry wins. A script referenced more than once is typically re-served from the browser cache, so a
137
+ later entry for the same URL would overwrite the real network timing with a near-instant one. */
138
+ if (!correlation.performance.value) {
139
+ correlation.performance.start = Math.floor(entry.startTime);
140
+ correlation.performance.end = Math.floor(entry.responseEnd);
141
+ correlation.performance.value = entry;
142
+ }
135
143
  }
136
144
 
137
145
  // Late-resolution subscribers can be for any asset type (not just scripts), so every entry is checked here,
@@ -285,7 +293,8 @@ function subscribeToLatePerformanceEntry(timings, mfeScriptUrl) {
285
293
 
286
294
  /**
287
295
  * Applies one manifest asset's performance entry to a timings object: weight/renderBlocking always accumulate;
288
- * fetchStart/fetchEnd and scriptStart/scriptEnd widen (never shrink) only when `timingMethod` calls for it; asset/
296
+ * fetchStart/fetchEnd and scriptStart/scriptEnd widen (never shrink) only when `timingMethod` calls for it, and only for
297
+ * the first entry seen per URL; asset/
289
298
  * type get anchored to the first script asset seen to resolve.
290
299
  * @param {RegisterAPITimings} timings
291
300
  * @param {PerformanceResourceTiming} entry
@@ -298,6 +307,14 @@ function applyManifestEntry(timings, entry, asset, entryState, timingMethod) {
298
307
  applyResourceWeight(timings, entry);
299
308
  if (timingMethod !== 'scripts' && timingMethod !== 'all') return; // no timing-widening effect at the 'entry' default/unset
300
309
 
310
+ /* Only the first entry per URL widens timing. A resource referenced more than once is typically re-served from the
311
+ browser cache with a later responseEnd, which would otherwise stretch fetchEnd out to the duplicate's load. */
312
+ const {
313
+ widenedAssetUrls
314
+ } = getOrCreateInternals(timings);
315
+ const url = cleanURL(entry.name);
316
+ if (widenedAssetUrls.has(url)) return;
317
+ widenedAssetUrls.add(url);
301
318
  const widensAllAssets = timingMethod === 'all';
302
319
  // Under 'scripts', only script assets widen the fetch window; under 'all', every matched asset does.
303
320
  if (widensAllAssets || asset.isScript) {
@@ -339,33 +356,45 @@ function applyManifestEntry(timings, entry, asset, entryState, timingMethod) {
339
356
  }
340
357
 
341
358
  /**
342
- * Subscribes to late resource timing emissions for manifest assets not yet resolved against the buffered entries.
343
- * Reuses the shared page-wide scriptObserver/poSubscribers mechanism (one PerformanceObserver for all MFEs, not
344
- * one per MFE) and, unlike that observer's own correlation bookkeeping, checks every resource entry -- not just
345
- * script-like ones -- so lazy-loaded images/fonts/stylesheets resolve too.
359
+ * Finds the manifest asset a resource entry should be credited to. An entry matching several assets is applied once;
360
+ * a script asset is preferred so an overlapping non-script matcher can't cause 'scripts' mode to skip a script entry.
361
+ * @param {import('./manifest').ParsedManifestAsset[]} assets
362
+ * @param {PerformanceResourceTiming} entry
363
+ * @returns {import('./manifest').ParsedManifestAsset|undefined}
364
+ */
365
+ function findMatchingManifestAsset(assets, entry) {
366
+ const matches = assets.filter(asset => asset.test(entry.name));
367
+ return matches.find(asset => asset.isScript) || matches[0];
368
+ }
369
+
370
+ /**
371
+ * Subscribes to late resource timing emissions for manifest assets. Reuses the shared page-wide
372
+ * scriptObserver/poSubscribers mechanism (one PerformanceObserver for all MFEs, not one per MFE) and, unlike that
373
+ * observer's own correlation bookkeeping, checks every resource entry -- not just script-like ones -- so lazy-loaded
374
+ * images/fonts/stylesheets resolve too. A matcher can legitimately match many resources (e.g. a hashed-chunk RegExp),
375
+ * so this subscriber never reports itself as finished; it is cleared by the shared observer's expiry instead.
346
376
  * @param {RegisterAPITimings} timings
347
- * @param {Set<import('./manifest').ParsedManifestAsset>} pending - manifest assets still unresolved
377
+ * @param {import('./manifest').ParsedManifestAsset[]} assets - every manifest asset to match late entries against
348
378
  * @param {{ resolved: boolean }} entryState - shared "first script asset wins" guard for a single `applyManifestTimings` call
349
379
  * @param {'entry'|'scripts'|'all'} [timingMethod] - forwarded to `applyManifestEntry` for each late-resolving asset
350
380
  */
351
- function subscribeToLateManifestEntries(timings, pending, entryState, timingMethod) {
381
+ function subscribeToLateManifestEntries(timings, assets, entryState, timingMethod) {
352
382
  if (!globalScope.PerformanceObserver?.supportedEntryTypes?.includes('resource')) return;
353
383
  poSubscribers.push({
354
384
  addedAt: now(),
355
385
  test: entry => {
356
- const matched = [...pending].find(asset => asset.test(entry.name));
357
- if (matched) {
358
- applyManifestEntry(timings, entry, matched, entryState, timingMethod);
359
- pending.delete(matched);
360
- }
361
- return pending.size === 0;
386
+ const matched = findMatchingManifestAsset(assets, entry);
387
+ if (matched) applyManifestEntry(timings, entry, matched, entryState, timingMethod);
388
+ return false;
362
389
  }
363
390
  });
364
391
  }
365
392
 
366
393
  /**
367
394
  * Applies a registered MFE's manifest to a timings object (already populated by `findScriptTimings`). No-op if no
368
- * manifest is present. Weight/renderBlocking always accumulate from every detected manifest asset; timing widening
395
+ * manifest is present. Every resource entry matching any manifest asset contributes -- an asset is not consumed by its
396
+ * first match, so a broad matcher (e.g. a RegExp for hashed chunks) credits all of the chunks it matches.
397
+ * Weight/renderBlocking always accumulate from every detected manifest asset; timing widening
369
398
  * (fetchStart/fetchEnd/scriptStart/scriptEnd/asset anchor) is opt-in via `timingMethod` -- see `applyManifestEntry`.
370
399
  * @param {RegisterAPITimings} timings - the timings object to widen in place
371
400
  * @param {RegisterAPITarget} target - the registered MFE target, which may carry a parsed `manifest`
@@ -376,16 +405,18 @@ export function applyManifestTimings(timings, target) {
376
405
  const entryState = {
377
406
  resolved: false
378
407
  };
379
- const pending = new Set(parsedManifest.assets);
408
+ const {
409
+ assets
410
+ } = parsedManifest;
380
411
  const resourceEntries = globalScope.performance?.getEntriesByType('resource') || [];
381
412
  resourceEntries.forEach(entry => {
382
- const matched = [...pending].find(asset => asset.test(entry.name));
383
- if (matched) {
384
- applyManifestEntry(timings, entry, matched, entryState, target.timingMethod);
385
- pending.delete(matched);
386
- }
413
+ const matched = findMatchingManifestAsset(assets, entry);
414
+ if (matched) applyManifestEntry(timings, entry, matched, entryState, target.timingMethod);
387
415
  });
388
- if (pending.size) subscribeToLateManifestEntries(timings, pending, entryState, target.timingMethod);
416
+
417
+ // Always subscribe: assets may still match resources that load after this point, and (unlike an exact matcher) a
418
+ // wildcard can't know its last match has been seen.
419
+ subscribeToLateManifestEntries(timings, assets, entryState, target.timingMethod);
389
420
  }
390
421
 
391
422
  /**