@newrelic/browser-agent 1.323.0-rc.9 → 1.324.0-rc.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.
- package/CHANGELOG.md +15 -0
- package/dist/cjs/common/constants/agent-constants.js +5 -1
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/util/attribute-size.js +5 -2
- package/dist/cjs/common/v2/manifest.js +1 -1
- package/dist/cjs/common/v2/script-tracker-types.js +3 -0
- package/dist/cjs/common/v2/script-tracker.js +55 -24
- package/dist/cjs/features/jserrors/aggregate/index.js +22 -0
- package/dist/cjs/features/utils/aggregate-base.js +10 -1
- package/dist/esm/common/constants/agent-constants.js +4 -0
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/util/attribute-size.js +5 -2
- package/dist/esm/common/v2/manifest.js +1 -1
- package/dist/esm/common/v2/script-tracker-types.js +3 -0
- package/dist/esm/common/v2/script-tracker.js +55 -24
- package/dist/esm/features/jserrors/aggregate/index.js +22 -0
- package/dist/esm/features/utils/aggregate-base.js +11 -2
- package/dist/types/common/constants/agent-constants.d.ts +4 -0
- package/dist/types/common/constants/agent-constants.d.ts.map +1 -1
- package/dist/types/common/util/attribute-size.d.ts.map +1 -1
- package/dist/types/common/v2/manifest.d.ts +1 -1
- package/dist/types/common/v2/script-tracker-types.d.ts +6 -0
- package/dist/types/common/v2/script-tracker-types.d.ts.map +1 -1
- package/dist/types/common/v2/script-tracker.d.ts +3 -1
- package/dist/types/common/v2/script-tracker.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts +5 -1
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/constants/agent-constants.js +4 -0
- package/src/common/util/attribute-size.js +5 -2
- package/src/common/v2/manifest.js +1 -1
- package/src/common/v2/script-tracker-types.js +3 -0
- package/src/common/v2/script-tracker.js +51 -24
- package/src/features/jserrors/aggregate/index.js +22 -0
- package/src/features/utils/aggregate-base.js +11 -2
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.
|
|
20
|
+
const VERSION = exports.VERSION = "1.324.0-rc.0";
|
|
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.
|
|
20
|
+
const VERSION = exports.VERSION = "1.324.0-rc.0";
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
correlation.performance.value
|
|
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
|
|
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
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
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 {
|
|
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,
|
|
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 =
|
|
367
|
-
if (matched)
|
|
368
|
-
|
|
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.
|
|
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
|
|
418
|
+
const {
|
|
419
|
+
assets
|
|
420
|
+
} = parsedManifest;
|
|
390
421
|
const resourceEntries = _runtime.globalScope.performance?.getEntriesByType('resource') || [];
|
|
391
422
|
resourceEntries.forEach(entry => {
|
|
392
|
-
const matched =
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -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
|
}
|
|
@@ -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 = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2020-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
correlation.performance.value
|
|
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
|
|
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
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
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 {
|
|
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,
|
|
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 =
|
|
357
|
-
if (matched)
|
|
358
|
-
|
|
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.
|
|
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
|
|
408
|
+
const {
|
|
409
|
+
assets
|
|
410
|
+
} = parsedManifest;
|
|
380
411
|
const resourceEntries = globalScope.performance?.getEntriesByType('resource') || [];
|
|
381
412
|
resourceEntries.forEach(entry => {
|
|
382
|
-
const matched =
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -21,6 +21,7 @@ import { getRegisteredTargetsFromFilename, getVersion2Attributes, getVersion2Dup
|
|
|
21
21
|
import { buildCauseString } from './cause-string';
|
|
22
22
|
import { ShortCircuit } from '../../../common/util/short-circuit';
|
|
23
23
|
import { EVENT_TYPES } from '../../../common/constants/events';
|
|
24
|
+
import { SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/session/constants';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* @typedef {import('./compute-stack-trace.js').StackInfo} StackInfo
|
|
@@ -45,6 +46,27 @@ export class Aggregate extends AggregateBase {
|
|
|
45
46
|
register('ierr', this.processError.bind(this), this.featureName, this.ee);
|
|
46
47
|
register('returnJserror', (jsErrorEvent, softNavAttrs) => this.#storeJserrorForHarvest(jsErrorEvent, softNavAttrs), this.featureName, this.ee);
|
|
47
48
|
|
|
49
|
+
/*
|
|
50
|
+
* The backend only remembers a stack trace for as long as the session that first reported it.
|
|
51
|
+
* The SessionEntity emits SESSION_EVENTS.RESET whenever the session expires (max duration or
|
|
52
|
+
* inactivity) or is otherwise reset, which is the same boundary the backend uses to forget
|
|
53
|
+
* previously-seen stack hashes. Forget them here too, so the next occurrence of an
|
|
54
|
+
* already-seen error resends its full stack_trace once, instead of a browser_stack_hash the
|
|
55
|
+
* backend can no longer resolve.
|
|
56
|
+
*/
|
|
57
|
+
const forgetReportedStacks = () => {
|
|
58
|
+
this.stackReported = {};
|
|
59
|
+
this.observedAt = {};
|
|
60
|
+
};
|
|
61
|
+
let currentSessionId = this.agentRef.runtime.session?.state?.value;
|
|
62
|
+
this.ee.on(SESSION_EVENTS.RESET, forgetReportedStacks);
|
|
63
|
+
this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
|
|
64
|
+
if (!data?.value) return;
|
|
65
|
+
/* A reset in another tab arrived as a cross-tab update carrying a new session id. */
|
|
66
|
+
if (type === SESSION_EVENT_TYPES.CROSS_TAB && currentSessionId && data.value !== currentSessionId) forgetReportedStacks();
|
|
67
|
+
currentSessionId = data.value; // also tracks same-tab writes, so a local reset's new id is picked up
|
|
68
|
+
});
|
|
69
|
+
|
|
48
70
|
// 0 == off, 1 == on
|
|
49
71
|
this.waitForFlags(['err']).then(([errFlag]) => {
|
|
50
72
|
if (errFlag) {
|
|
@@ -9,8 +9,9 @@ import { EventBuffer } from './event-buffer';
|
|
|
9
9
|
import { handle } from '../../common/event-emitter/handle';
|
|
10
10
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../metrics/constants';
|
|
11
11
|
import { EventAggregator } from '../../common/aggregate/event-aggregator';
|
|
12
|
-
import { MAX_PAYLOAD_SIZE, IDEAL_PAYLOAD_SIZE, SUPPORTS_REGISTERED_ENTITIES } from '../../common/constants/agent-constants';
|
|
12
|
+
import { MAX_PAYLOAD_SIZE, IDEAL_PAYLOAD_SIZE, MIN_EARLY_HARVEST_INTERVAL, EARLY_HARVEST_BYPASS_SIZE, SUPPORTS_REGISTERED_ENTITIES } from '../../common/constants/agent-constants';
|
|
13
13
|
import { warn } from '../../common/util/console';
|
|
14
|
+
import { now } from '../../common/timing/now';
|
|
14
15
|
export class AggregateBase extends FeatureBase {
|
|
15
16
|
/**
|
|
16
17
|
* Create an AggregateBase instance.
|
|
@@ -26,6 +27,8 @@ export class AggregateBase extends FeatureBase {
|
|
|
26
27
|
this.canHarvestEarly = true;
|
|
27
28
|
/** @type {Boolean} indicates if the feature is actively in a retry deferral period */
|
|
28
29
|
this.isRetrying = false;
|
|
30
|
+
/** @type {number|undefined} the timestamp (ms, relative to page start) of the last early harvest triggered by this feature, if any */
|
|
31
|
+
this.lastEarlyHarvestAt = undefined;
|
|
29
32
|
this.harvestOpts = {}; // features aggregate classes can define custom opts for when their harvest is called
|
|
30
33
|
|
|
31
34
|
this.#setupEventStore();
|
|
@@ -81,13 +84,19 @@ export class AggregateBase extends FeatureBase {
|
|
|
81
84
|
|
|
82
85
|
/**
|
|
83
86
|
* 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.
|
|
84
|
-
* If conditions are met, a new harvest will be triggered immediately.
|
|
87
|
+
* 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.
|
|
88
|
+
* 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`
|
|
89
|
+
* 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.
|
|
85
90
|
* @returns void
|
|
86
91
|
*/
|
|
87
92
|
decideEarlyHarvest() {
|
|
88
93
|
if (!this.canHarvestEarly || this.blocked || this.isRetrying) return;
|
|
89
94
|
const estimatedSize = this.events.byteSize() + (this.customAttributesAreSeparate ? this.agentRef.runtime.jsAttributesMetadata.bytes : 0);
|
|
90
95
|
if (estimatedSize > IDEAL_PAYLOAD_SIZE) {
|
|
96
|
+
const currentTime = now();
|
|
97
|
+
/* 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. */
|
|
98
|
+
if (estimatedSize < EARLY_HARVEST_BYPASS_SIZE && this.lastEarlyHarvestAt !== undefined && currentTime - this.lastEarlyHarvestAt < MIN_EARLY_HARVEST_INTERVAL) return;
|
|
99
|
+
this.lastEarlyHarvestAt = currentTime;
|
|
91
100
|
this.agentRef.runtime.harvester.triggerHarvestFor(this);
|
|
92
101
|
this.reportSupportabilityMetric("".concat(this.featureName, "/Harvest/Early/Seen"), estimatedSize);
|
|
93
102
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export const IDEAL_PAYLOAD_SIZE: 16000;
|
|
2
2
|
export const MAX_PAYLOAD_SIZE: 1000000;
|
|
3
|
+
/** The minimum time (ms) between early harvests of a single feature. Data that arrives inside this window stays buffered for the next harvest. */
|
|
4
|
+
export const MIN_EARLY_HARVEST_INTERVAL: 500;
|
|
5
|
+
/** 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. */
|
|
6
|
+
export const EARLY_HARVEST_BYPASS_SIZE: number;
|
|
3
7
|
export const DEFAULT_KEY: "NR_CONTAINER_AGENT";
|
|
4
8
|
export const SESSION_ERROR: "SESSION_ERROR";
|
|
5
9
|
export const SUPPORTS_REGISTERED_ENTITIES: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-constants.d.ts","sourceRoot":"","sources":["../../../../src/common/constants/agent-constants.js"],"names":[],"mappings":"AAMA,iCAAkC,KAAK,CAAA;AACvC,+BAAgC,OAAO,CAAA;AACvC,0BAA2B,oBAAoB,CAAA;AAC/C,4BAA6B,eAAe,CAAA;AAE5C;IACE,CAAC,aAAa,CAAC,OAAO,CAAC,UAAM;IAC7B,CAAC,aAAa,CAAC,aAAa,CAAC,UAAM;IACnC,CAAC,aAAa,CAAC,QAAQ,CAAC,UAAM;IAC9B,CAAC,aAAa,CAAC,IAAI,CAAC,UAAM;EAC3B;
|
|
1
|
+
{"version":3,"file":"agent-constants.d.ts","sourceRoot":"","sources":["../../../../src/common/constants/agent-constants.js"],"names":[],"mappings":"AAMA,iCAAkC,KAAK,CAAA;AACvC,+BAAgC,OAAO,CAAA;AACvC,kJAAkJ;AAClJ,yCAA0C,GAAG,CAAA;AAC7C,sNAAsN;AACtN,+CAA6D;AAC7D,0BAA2B,oBAAoB,CAAA;AAC/C,4BAA6B,eAAe,CAAA;AAE5C;IACE,CAAC,aAAa,CAAC,OAAO,CAAC,UAAM;IAC7B,CAAC,aAAa,CAAC,aAAa,CAAC,UAAM;IACnC,CAAC,aAAa,CAAC,QAAQ,CAAC,UAAM;IAC9B,CAAC,aAAa,CAAC,IAAI,CAAC,UAAM;EAC3B;8BAhB6B,iCAAiC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attribute-size.d.ts","sourceRoot":"","sources":["../../../../src/common/util/attribute-size.js"],"names":[],"mappings":"AAMA;;
|
|
1
|
+
{"version":3,"file":"attribute-size.d.ts","sourceRoot":"","sources":["../../../../src/common/util/attribute-size.js"],"names":[],"mappings":"AAMA;;EAoBC"}
|
|
@@ -52,7 +52,7 @@ export type ParsedManifest = {
|
|
|
52
52
|
};
|
|
53
53
|
export type AssetFile = {
|
|
54
54
|
/**
|
|
55
|
-
* - the path/path-fragment string, or RegExp, used to match a resolved URL against this asset
|
|
55
|
+
* - 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
|
|
56
56
|
*/
|
|
57
57
|
matcher: string | RegExp;
|
|
58
58
|
/**
|
|
@@ -15,6 +15,12 @@ export type TimingsInternals = {
|
|
|
15
15
|
* script itself) is never counted twice.
|
|
16
16
|
*/
|
|
17
17
|
weighedAssetUrls: Set<string>;
|
|
18
|
+
/**
|
|
19
|
+
* - 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.
|
|
22
|
+
*/
|
|
23
|
+
widenedAssetUrls: Set<string>;
|
|
18
24
|
/**
|
|
19
25
|
* - Always present (seeded by
|
|
20
26
|
* `getOrCreateInternals`, see `script-tracker.js`). `findScriptTimings` overrides the seeded default with one that
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"script-tracker-types.d.ts","sourceRoot":"","sources":["../../../../src/common/v2/script-tracker-types.js"],"names":[],"mappings":";;;;;;;;2CAMa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI;;;;;;;;sBAQnC,GAAG,CAAC,MAAM,CAAC;;;;;;
|
|
1
|
+
{"version":3,"file":"script-tracker-types.d.ts","sourceRoot":"","sources":["../../../../src/common/v2/script-tracker-types.js"],"names":[],"mappings":";;;;;;;;2CAMa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI;;;;;;;;sBAQnC,GAAG,CAAC,MAAM,CAAC;;;;;;sBAIX,GAAG,CAAC,MAAM,CAAC;;;;;;gCAGX,4BAA4B"}
|
|
@@ -19,7 +19,9 @@ export function extractUrlsFromStack(stack: string): string[];
|
|
|
19
19
|
export function getDeepStackTrace(): Error.stack | undefined;
|
|
20
20
|
/**
|
|
21
21
|
* Applies a registered MFE's manifest to a timings object (already populated by `findScriptTimings`). No-op if no
|
|
22
|
-
* manifest is present.
|
|
22
|
+
* manifest is present. Every resource entry matching any manifest asset contributes -- an asset is not consumed by its
|
|
23
|
+
* first match, so a broad matcher (e.g. a RegExp for hashed chunks) credits all of the chunks it matches.
|
|
24
|
+
* Weight/renderBlocking always accumulate from every detected manifest asset; timing widening
|
|
23
25
|
* (fetchStart/fetchEnd/scriptStart/scriptEnd/asset anchor) is opt-in via `timingMethod` -- see `applyManifestEntry`.
|
|
24
26
|
* @param {RegisterAPITimings} timings - the timings object to widen in place
|
|
25
27
|
* @param {RegisterAPITarget} target - the registered MFE target, which may carry a parsed `manifest`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"script-tracker.d.ts","sourceRoot":"","sources":["../../../../src/common/v2/script-tracker.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"script-tracker.d.ts","sourceRoot":"","sources":["../../../../src/common/v2/script-tracker.js"],"names":[],"mappings":"AAmEA;;;;;;GAMG;AACH,2CAHW,MAAM,GACJ,iBAAiB,GAAG,SAAS,CAIzC;AAoFD;;;;GAIG;AACH,4CAHW,MAAM,GACJ,MAAM,EAAE,CAsBpB;AAED;;;GAGG;AACH,qCAFa,KAAK,CAAC,KAAK,GAAG,SAAS,CAanC;AA0LD;;;;;;;;GAQG;AACH,8CAHW,kBAAkB,UAClB,iBAAiB,QAkB3B;AAED;;;;;GAKG;AACH,2CAJW,iBAAiB,GAEf,kBAAkB,CAkG9B;AAxfD;;;;;GAKG;AAEH,kCAAkC;AAClC,yBAAmB;AAUnB,iIAAiI;AACjI,iCADW,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CACE;iCAlB9B,OAAO,sCAAsC,EAAE,kBAAkB;gCACjE,OAAO,sCAAsC,EAAE,iBAAiB;2CAChE,OAAO,wBAAwB,EAAE,4BAA4B;+BAC7D,OAAO,wBAAwB,EAAE,gBAAgB;kCAR5B,sBAAsB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/jserrors/aggregate/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/jserrors/aggregate/index.js"],"names":[],"mappings":"AA2BA;;GAEG;AAEH;IACE,2BAAiC;IACjC,2BAgDC;IA3CC,kBAAuB;IACvB,eAAoB;IACpB,qBAA0B;IAC1B,qBAAwB;IAGxB,4BAAgE;IAChE,0BAA+D;IAsCjE;;;;MAMC;IAED;;;MAcC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,kBATW,KAAK,GAAC,aAAa,SACnB,MAAM,aACN,OAAO,qBACP,MAAM,cACN,OAAO,kBACP,MAAM,WACN,MAAM,GACJ,IAAI,CAiBhB;;CAyQF;wBAnYY,OAAO,0BAA0B,EAAE,SAAS;8BAX3B,4BAA4B;2BAE/B,gCAAgC"}
|
|
@@ -11,6 +11,8 @@ export class AggregateBase extends FeatureBase {
|
|
|
11
11
|
canHarvestEarly: boolean;
|
|
12
12
|
/** @type {Boolean} indicates if the feature is actively in a retry deferral period */
|
|
13
13
|
isRetrying: boolean;
|
|
14
|
+
/** @type {number|undefined} the timestamp (ms, relative to page start) of the last early harvest triggered by this feature, if any */
|
|
15
|
+
lastEarlyHarvestAt: number | undefined;
|
|
14
16
|
harvestOpts: {};
|
|
15
17
|
events: any;
|
|
16
18
|
/** @type {Boolean} indicates if the feature supports registered entities and the harvest requirements therein. Also read by getter "harvestEndpointVersion". Controlled by feature flag in pre-release phase. */
|
|
@@ -25,7 +27,9 @@ export class AggregateBase extends FeatureBase {
|
|
|
25
27
|
drained: boolean | undefined;
|
|
26
28
|
/**
|
|
27
29
|
* 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.
|
|
28
|
-
* If conditions are met, a new harvest will be triggered immediately.
|
|
30
|
+
* 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.
|
|
31
|
+
* 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`
|
|
32
|
+
* 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.
|
|
29
33
|
* @returns void
|
|
30
34
|
*/
|
|
31
35
|
decideEarlyHarvest(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aggregate-base.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/aggregate-base.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"aggregate-base.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/aggregate-base.js"],"names":[],"mappings":"AAeA;IACE;;;;OAIG;IACH,sBAHW,MAAM,eACN,MAAM,EAmBhB;IAdC,uOAAuO;IACvO,qCAAwC;IACxC,gLAAgL;IAChL,yBAA2B;IAC3B,sFAAsF;IACtF,oBAAuB;IACvB,sIAAsI;IACtI,oBADW,MAAM,GAAC,SAAS,CACQ;IAEnC,gBAAqB;IAqBjB,YAAsE;IAW5E,iNAAiN;IACjN,0CAEC;IAED;;;;OAIG;IACH,8BAHU,MAAM,CAKf;IAED,qBAOC;IAFG,6BAAmB;IAIvB;;;;;;OAMG;IACH,2BAWC;IAED;;;;OAIG;IACH,yBAHW,MAAM,EAAE,gBAyBlB;IAED;;OAEG;IACH,cAEC;IAED,qCAEC;IAED;;;;;;OAMG;IACH,uDAJW,MAAM,GAAC,SAAS,SAsB1B;IAED;;;;;OAKG;IACH,4BAHG;QAAyB,IAAI,GAArB,OAAO,YAAC;QACS,KAAK,GAAtB,OAAO,YAAC;KAClB,QAKA;IAED;;;;OAIG;IACH,uCAHW,GAAC,UACD,GAAC,QAIX;;CACF;4BA/L2B,gBAAgB"}
|
package/package.json
CHANGED
|
@@ -6,6 +6,10 @@ import { FEATURE_NAMES } from '../../loaders/features/features'
|
|
|
6
6
|
|
|
7
7
|
export const IDEAL_PAYLOAD_SIZE = 16000
|
|
8
8
|
export const MAX_PAYLOAD_SIZE = 1000000
|
|
9
|
+
/** The minimum time (ms) between early harvests of a single feature. Data that arrives inside this window stays buffered for the next harvest. */
|
|
10
|
+
export const MIN_EARLY_HARVEST_INTERVAL = 500
|
|
11
|
+
/** 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. */
|
|
12
|
+
export const EARLY_HARVEST_BYPASS_SIZE = MAX_PAYLOAD_SIZE / 2
|
|
9
13
|
export const DEFAULT_KEY = 'NR_CONTAINER_AGENT'
|
|
10
14
|
export const SESSION_ERROR = 'SESSION_ERROR'
|
|
11
15
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2020-
|
|
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'
|
|
@@ -10,12 +10,15 @@ export function trackObjectAttributeSize (parent, object) {
|
|
|
10
10
|
// proxy attribute to calculate its size when changed
|
|
11
11
|
parent[object] = new Proxy(originalAttribute, {
|
|
12
12
|
set (target, prop, value) {
|
|
13
|
+
/* 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. */
|
|
14
|
+
if (Object.prototype.hasOwnProperty.call(target, prop)) output.bytes -= prop.length + stringify(target[prop]).length
|
|
13
15
|
output.bytes += prop.length + stringify(value).length
|
|
14
16
|
target[prop] = value
|
|
15
17
|
return true
|
|
16
18
|
},
|
|
17
19
|
deleteProperty (target, prop) {
|
|
18
|
-
|
|
20
|
+
/* Deleting a key that was never set removes nothing, so the count must not change. */
|
|
21
|
+
if (Object.prototype.hasOwnProperty.call(target, prop)) output.bytes -= prop.length + stringify(target[prop]).length
|
|
19
22
|
return delete target[prop]
|
|
20
23
|
}
|
|
21
24
|
})
|
|
@@ -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
|
|
@@ -104,6 +105,9 @@ if (globalScope.MutationObserver && globalScope.document) {
|
|
|
104
105
|
if (node.nodeName === 'SCRIPT' && node.src) {
|
|
105
106
|
const cleanedSrc = cleanURL(node.src)
|
|
106
107
|
const correlation = getOrCreateCorrelation(cleanedSrc)
|
|
108
|
+
/* The first node wins, same as the performance entry below. Skipping here also means no load/error listeners
|
|
109
|
+
are attached for duplicate references. */
|
|
110
|
+
if (correlation.dom.value) return
|
|
107
111
|
|
|
108
112
|
correlation.dom.start = now()
|
|
109
113
|
correlation.dom.value = node
|
|
@@ -131,9 +135,13 @@ if (globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
|
|
|
131
135
|
if (validEntryCriteria(entry)) {
|
|
132
136
|
const entryUrl = cleanURL(entry.name)
|
|
133
137
|
const correlation = getOrCreateCorrelation(entryUrl)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
correlation.performance.value
|
|
138
|
+
/* The first entry wins. A script referenced more than once is typically re-served from the browser cache, so a
|
|
139
|
+
later entry for the same URL would overwrite the real network timing with a near-instant one. */
|
|
140
|
+
if (!correlation.performance.value) {
|
|
141
|
+
correlation.performance.start = Math.floor(entry.startTime)
|
|
142
|
+
correlation.performance.end = Math.floor(entry.responseEnd)
|
|
143
|
+
correlation.performance.value = entry
|
|
144
|
+
}
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
// 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
|
|
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
|
|
@@ -299,6 +308,13 @@ function applyManifestEntry (timings, entry, asset, entryState, timingMethod) {
|
|
|
299
308
|
|
|
300
309
|
if (timingMethod !== 'scripts' && timingMethod !== 'all') return // no timing-widening effect at the 'entry' default/unset
|
|
301
310
|
|
|
311
|
+
/* Only the first entry per URL widens timing. A resource referenced more than once is typically re-served from the
|
|
312
|
+
browser cache with a later responseEnd, which would otherwise stretch fetchEnd out to the duplicate's load. */
|
|
313
|
+
const { widenedAssetUrls } = getOrCreateInternals(timings)
|
|
314
|
+
const url = cleanURL(entry.name)
|
|
315
|
+
if (widenedAssetUrls.has(url)) return
|
|
316
|
+
widenedAssetUrls.add(url)
|
|
317
|
+
|
|
302
318
|
const widensAllAssets = timingMethod === 'all'
|
|
303
319
|
// Under 'scripts', only script assets widen the fetch window; under 'all', every matched asset does.
|
|
304
320
|
if (widensAllAssets || asset.isScript) {
|
|
@@ -335,34 +351,46 @@ function applyManifestEntry (timings, entry, asset, entryState, timingMethod) {
|
|
|
335
351
|
}
|
|
336
352
|
|
|
337
353
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
354
|
+
* Finds the manifest asset a resource entry should be credited to. An entry matching several assets is applied once;
|
|
355
|
+
* a script asset is preferred so an overlapping non-script matcher can't cause 'scripts' mode to skip a script entry.
|
|
356
|
+
* @param {import('./manifest').ParsedManifestAsset[]} assets
|
|
357
|
+
* @param {PerformanceResourceTiming} entry
|
|
358
|
+
* @returns {import('./manifest').ParsedManifestAsset|undefined}
|
|
359
|
+
*/
|
|
360
|
+
function findMatchingManifestAsset (assets, entry) {
|
|
361
|
+
const matches = assets.filter(asset => asset.test(entry.name))
|
|
362
|
+
return matches.find(asset => asset.isScript) || matches[0]
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Subscribes to late resource timing emissions for manifest assets. Reuses the shared page-wide
|
|
367
|
+
* scriptObserver/poSubscribers mechanism (one PerformanceObserver for all MFEs, not one per MFE) and, unlike that
|
|
368
|
+
* observer's own correlation bookkeeping, checks every resource entry -- not just script-like ones -- so lazy-loaded
|
|
369
|
+
* images/fonts/stylesheets resolve too. A matcher can legitimately match many resources (e.g. a hashed-chunk RegExp),
|
|
370
|
+
* so this subscriber never reports itself as finished; it is cleared by the shared observer's expiry instead.
|
|
342
371
|
* @param {RegisterAPITimings} timings
|
|
343
|
-
* @param {
|
|
372
|
+
* @param {import('./manifest').ParsedManifestAsset[]} assets - every manifest asset to match late entries against
|
|
344
373
|
* @param {{ resolved: boolean }} entryState - shared "first script asset wins" guard for a single `applyManifestTimings` call
|
|
345
374
|
* @param {'entry'|'scripts'|'all'} [timingMethod] - forwarded to `applyManifestEntry` for each late-resolving asset
|
|
346
375
|
*/
|
|
347
|
-
function subscribeToLateManifestEntries (timings,
|
|
376
|
+
function subscribeToLateManifestEntries (timings, assets, entryState, timingMethod) {
|
|
348
377
|
if (!globalScope.PerformanceObserver?.supportedEntryTypes?.includes('resource')) return
|
|
349
378
|
|
|
350
379
|
poSubscribers.push({
|
|
351
380
|
addedAt: now(),
|
|
352
381
|
test: (entry) => {
|
|
353
|
-
const matched =
|
|
354
|
-
if (matched)
|
|
355
|
-
|
|
356
|
-
pending.delete(matched)
|
|
357
|
-
}
|
|
358
|
-
return pending.size === 0
|
|
382
|
+
const matched = findMatchingManifestAsset(assets, entry)
|
|
383
|
+
if (matched) applyManifestEntry(timings, entry, matched, entryState, timingMethod)
|
|
384
|
+
return false
|
|
359
385
|
}
|
|
360
386
|
})
|
|
361
387
|
}
|
|
362
388
|
|
|
363
389
|
/**
|
|
364
390
|
* Applies a registered MFE's manifest to a timings object (already populated by `findScriptTimings`). No-op if no
|
|
365
|
-
* manifest is present.
|
|
391
|
+
* manifest is present. Every resource entry matching any manifest asset contributes -- an asset is not consumed by its
|
|
392
|
+
* first match, so a broad matcher (e.g. a RegExp for hashed chunks) credits all of the chunks it matches.
|
|
393
|
+
* Weight/renderBlocking always accumulate from every detected manifest asset; timing widening
|
|
366
394
|
* (fetchStart/fetchEnd/scriptStart/scriptEnd/asset anchor) is opt-in via `timingMethod` -- see `applyManifestEntry`.
|
|
367
395
|
* @param {RegisterAPITimings} timings - the timings object to widen in place
|
|
368
396
|
* @param {RegisterAPITarget} target - the registered MFE target, which may carry a parsed `manifest`
|
|
@@ -372,18 +400,17 @@ export function applyManifestTimings (timings, target) {
|
|
|
372
400
|
if (!parsedManifest || !parsedManifest.assets.length) return
|
|
373
401
|
|
|
374
402
|
const entryState = { resolved: false }
|
|
375
|
-
const
|
|
403
|
+
const { assets } = parsedManifest
|
|
376
404
|
|
|
377
405
|
const resourceEntries = globalScope.performance?.getEntriesByType('resource') || []
|
|
378
406
|
resourceEntries.forEach((entry) => {
|
|
379
|
-
const matched =
|
|
380
|
-
if (matched)
|
|
381
|
-
applyManifestEntry(timings, entry, matched, entryState, target.timingMethod)
|
|
382
|
-
pending.delete(matched)
|
|
383
|
-
}
|
|
407
|
+
const matched = findMatchingManifestAsset(assets, entry)
|
|
408
|
+
if (matched) applyManifestEntry(timings, entry, matched, entryState, target.timingMethod)
|
|
384
409
|
})
|
|
385
410
|
|
|
386
|
-
|
|
411
|
+
// Always subscribe: assets may still match resources that load after this point, and (unlike an exact matcher) a
|
|
412
|
+
// wildcard can't know its last match has been seen.
|
|
413
|
+
subscribeToLateManifestEntries(timings, assets, entryState, target.timingMethod)
|
|
387
414
|
}
|
|
388
415
|
|
|
389
416
|
/**
|
|
@@ -23,6 +23,7 @@ import { getRegisteredTargetsFromFilename, getVersion2Attributes, getVersion2Dup
|
|
|
23
23
|
import { buildCauseString } from './cause-string'
|
|
24
24
|
import { ShortCircuit } from '../../../common/util/short-circuit'
|
|
25
25
|
import { EVENT_TYPES } from '../../../common/constants/events'
|
|
26
|
+
import { SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/session/constants'
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* @typedef {import('./compute-stack-trace.js').StackInfo} StackInfo
|
|
@@ -48,6 +49,27 @@ export class Aggregate extends AggregateBase {
|
|
|
48
49
|
register('ierr', this.processError.bind(this), this.featureName, this.ee)
|
|
49
50
|
register('returnJserror', (jsErrorEvent, softNavAttrs) => this.#storeJserrorForHarvest(jsErrorEvent, softNavAttrs), this.featureName, this.ee)
|
|
50
51
|
|
|
52
|
+
/*
|
|
53
|
+
* The backend only remembers a stack trace for as long as the session that first reported it.
|
|
54
|
+
* The SessionEntity emits SESSION_EVENTS.RESET whenever the session expires (max duration or
|
|
55
|
+
* inactivity) or is otherwise reset, which is the same boundary the backend uses to forget
|
|
56
|
+
* previously-seen stack hashes. Forget them here too, so the next occurrence of an
|
|
57
|
+
* already-seen error resends its full stack_trace once, instead of a browser_stack_hash the
|
|
58
|
+
* backend can no longer resolve.
|
|
59
|
+
*/
|
|
60
|
+
const forgetReportedStacks = () => {
|
|
61
|
+
this.stackReported = {}
|
|
62
|
+
this.observedAt = {}
|
|
63
|
+
}
|
|
64
|
+
let currentSessionId = this.agentRef.runtime.session?.state?.value
|
|
65
|
+
this.ee.on(SESSION_EVENTS.RESET, forgetReportedStacks)
|
|
66
|
+
this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
|
|
67
|
+
if (!data?.value) return
|
|
68
|
+
/* A reset in another tab arrived as a cross-tab update carrying a new session id. */
|
|
69
|
+
if (type === SESSION_EVENT_TYPES.CROSS_TAB && currentSessionId && data.value !== currentSessionId) forgetReportedStacks()
|
|
70
|
+
currentSessionId = data.value // also tracks same-tab writes, so a local reset's new id is picked up
|
|
71
|
+
})
|
|
72
|
+
|
|
51
73
|
// 0 == off, 1 == on
|
|
52
74
|
this.waitForFlags(['err']).then(([errFlag]) => {
|
|
53
75
|
if (errFlag) {
|
|
@@ -9,8 +9,9 @@ import { EventBuffer } from './event-buffer'
|
|
|
9
9
|
import { handle } from '../../common/event-emitter/handle'
|
|
10
10
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../metrics/constants'
|
|
11
11
|
import { EventAggregator } from '../../common/aggregate/event-aggregator'
|
|
12
|
-
import { MAX_PAYLOAD_SIZE, IDEAL_PAYLOAD_SIZE, SUPPORTS_REGISTERED_ENTITIES } from '../../common/constants/agent-constants'
|
|
12
|
+
import { MAX_PAYLOAD_SIZE, IDEAL_PAYLOAD_SIZE, MIN_EARLY_HARVEST_INTERVAL, EARLY_HARVEST_BYPASS_SIZE, SUPPORTS_REGISTERED_ENTITIES } from '../../common/constants/agent-constants'
|
|
13
13
|
import { warn } from '../../common/util/console'
|
|
14
|
+
import { now } from '../../common/timing/now'
|
|
14
15
|
|
|
15
16
|
export class AggregateBase extends FeatureBase {
|
|
16
17
|
/**
|
|
@@ -27,6 +28,8 @@ export class AggregateBase extends FeatureBase {
|
|
|
27
28
|
this.canHarvestEarly = true
|
|
28
29
|
/** @type {Boolean} indicates if the feature is actively in a retry deferral period */
|
|
29
30
|
this.isRetrying = false
|
|
31
|
+
/** @type {number|undefined} the timestamp (ms, relative to page start) of the last early harvest triggered by this feature, if any */
|
|
32
|
+
this.lastEarlyHarvestAt = undefined
|
|
30
33
|
|
|
31
34
|
this.harvestOpts = {} // features aggregate classes can define custom opts for when their harvest is called
|
|
32
35
|
|
|
@@ -85,13 +88,19 @@ export class AggregateBase extends FeatureBase {
|
|
|
85
88
|
|
|
86
89
|
/**
|
|
87
90
|
* 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.
|
|
88
|
-
* If conditions are met, a new harvest will be triggered immediately.
|
|
91
|
+
* 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.
|
|
92
|
+
* 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`
|
|
93
|
+
* 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.
|
|
89
94
|
* @returns void
|
|
90
95
|
*/
|
|
91
96
|
decideEarlyHarvest () {
|
|
92
97
|
if (!this.canHarvestEarly || this.blocked || this.isRetrying) return
|
|
93
98
|
const estimatedSize = this.events.byteSize() + (this.customAttributesAreSeparate ? this.agentRef.runtime.jsAttributesMetadata.bytes : 0)
|
|
94
99
|
if (estimatedSize > IDEAL_PAYLOAD_SIZE) {
|
|
100
|
+
const currentTime = now()
|
|
101
|
+
/* 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. */
|
|
102
|
+
if (estimatedSize < EARLY_HARVEST_BYPASS_SIZE && this.lastEarlyHarvestAt !== undefined && currentTime - this.lastEarlyHarvestAt < MIN_EARLY_HARVEST_INTERVAL) return
|
|
103
|
+
this.lastEarlyHarvestAt = currentTime
|
|
95
104
|
this.agentRef.runtime.harvester.triggerHarvestFor(this)
|
|
96
105
|
this.reportSupportabilityMetric(`${this.featureName}/Harvest/Early/Seen`, estimatedSize)
|
|
97
106
|
}
|