@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
@@ -16,7 +16,7 @@ import { canCapturePayload, isLikelyHumanReadable, parseQueryString, createStrin
16
16
  import { Obfuscator } from '../../../common/util/obfuscate';
17
17
  import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate, isMfeTarget } from '../../../common/v2/utils';
18
18
  import { EVENT_TYPES } from '../../../common/constants/events';
19
- import { generateUuid } from '../../../common/ids/unique-id';
19
+ import { generateSpanId, generateTraceId, generateUuid } from '../../../common/ids/unique-id';
20
20
  export class Aggregate extends AggregateBase {
21
21
  static featureName = FEATURE_NAME;
22
22
  constructor(agentRef) {
@@ -126,10 +126,23 @@ export class Aggregate extends AggregateBase {
126
126
  ...event,
127
127
  targetAttributes: getVersion2Attributes(target, this)
128
128
  });
129
- if (shouldDuplicate(target, this)) this.reportContainerEvent({
130
- ...event,
131
- targetAttributes: getVersion2DuplicationAttributes(target, this)
132
- }, ctx);
129
+ if (shouldDuplicate(target, this)) {
130
+ const duplicateEvent = {
131
+ ...event,
132
+ targetAttributes: getVersion2DuplicationAttributes(target, this)
133
+ };
134
+ /** the container-duplicate copy represents the same single real HTTP call as the MFE event above, so it
135
+ * needs its own independent trace/span identity -- otherwise the two would collide on the same
136
+ * spanId/traceId, and BELC could not create two separate Spans/Traces from them. Only the id namespace is
137
+ * independent; spanTimestamp is intentionally left as-is (event.spanTimestamp), since it still reflects the
138
+ * real call's actual timing. When `event.spanId` was never set (e.g. distributed tracing is disabled), there
139
+ * is no span identity to duplicate either -- both copies correctly end up with no spanId/traceId. */
140
+ if (event.spanId) {
141
+ duplicateEvent.spanId = generateSpanId();
142
+ duplicateEvent.traceId = generateTraceId();
143
+ }
144
+ this.reportContainerEvent(duplicateEvent, ctx);
145
+ }
133
146
  } else {
134
147
  this.reportContainerEvent(event, ctx);
135
148
  }
@@ -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
  }
@@ -115,10 +115,13 @@ export class RegisteredIframeEntity {
115
115
  return;
116
116
  }
117
117
 
118
+ /* Script timings are found by parsing the call stack for the script that called register(), so this has to run
119
+ synchronously here. By the time the registration promise resolves, that caller is no longer on the stack. */
120
+ const timings = findScriptTimings(opts);
121
+
118
122
  // Store the registration promise so other methods can wait for it
119
123
  this.#registrationPromise = this.#register(opts);
120
124
  this.#registrationPromise.then(() => {
121
- const timings = findScriptTimings();
122
125
  // Send initial timing values, skipping ones still at their "not yet known" default
123
126
  // (e.g. reportedAt: undefined, fetchStart: 0) to avoid pointless postMessage calls
124
127
  for (const [key, value] of Object.entries(timings)) {
@@ -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;8BAZ6B,iCAAiC"}
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;;EAiBC"}
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;;;;;;gCAIX,4BAA4B"}
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. Weight/renderBlocking always accumulate from every detected manifest asset; timing widening
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":"AAkEA;;;;;;GAMG;AACH,2CAHW,MAAM,GACJ,iBAAiB,GAAG,SAAS,CAIzC;AA6ED;;;;GAIG;AACH,4CAHW,MAAM,GACJ,MAAM,EAAE,CAsBpB;AAED;;;GAGG;AACH,qCAFa,KAAK,CAAC,KAAK,GAAG,SAAS,CAanC;AAwKD;;;;;;GAMG;AACH,8CAHW,kBAAkB,UAClB,iBAAiB,QAmB3B;AAED;;;;;GAKG;AACH,2CAJW,iBAAiB,GAEf,kBAAkB,CAkG9B;AA7dD;;;;;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
+ {"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/ajax/aggregate/index.js"],"names":[],"mappings":"AAoBA;IACE,2BAAiC;IAEjC,2BA6BC;IAvBC,uBAA4D;IAyB9D,0GA2FC;IAED,+CAOC;IAED,iDA2EC;CACF;8BA9N6B,4BAA4B;2BAK/B,gCAAgC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/ajax/aggregate/index.js"],"names":[],"mappings":"AAoBA;IACE,2BAAiC;IAEjC,2BA6BC;IAvBC,uBAA4D;IAyB9D,0GAwGC;IAED,+CAOC;IAED,iDA2EC;CACF;8BA3O6B,4BAA4B;2BAK/B,gCAAgC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/jserrors/aggregate/index.js"],"names":[],"mappings":"AA0BA;;GAEG;AAEH;IACE,2BAAiC;IACjC,2BA2BC;IAtBC,kBAAuB;IACvB,eAAoB;IACpB,qBAA0B;IAC1B,qBAAwB;IAGxB,4BAAgE;IAChE,0BAA+D;IAiBjE;;;;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;wBA9WY,OAAO,0BAA0B,EAAE,SAAS;8BAV3B,4BAA4B;2BAE/B,gCAAgC"}
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":"AAcA;IACE;;;;OAIG;IACH,sBAHW,MAAM,eACN,MAAM,EAiBhB;IAZC,uOAAuO;IACvO,qCAAwC;IACxC,gLAAgL;IAChL,yBAA2B;IAC3B,sFAAsF;IACtF,oBAAuB;IAEvB,gBAAqB;IAqBjB,YAAsE;IAW5E,iNAAiN;IACjN,0CAEC;IAED;;;;OAIG;IACH,8BAHU,MAAM,CAKf;IAED,qBAOC;IAFG,6BAAmB;IAIvB;;;;OAIG;IACH,2BAOC;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;4BAtL2B,gBAAgB"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"registered-iframe-entity.d.ts","sourceRoot":"","sources":["../../../src/interfaces/registered-iframe-entity.js"],"names":[],"mappings":"AAuBA;;;;GAIG;AAEH;;;;;;GAMG;AACH;IAkDE;;;OAGG;IACH,kBAFW,sBAAsB,EAsDhC;IAzGD,kCAAkC;IAClC,UADW,mBAAmB,CAW7B;IAyBD,mBANa,OAAO,EAQnB;IAbD;;;;;;OAMG;IACH,eAFa,OAAO,CAInB;IAoTD;;;;;OAKG;IACH,oBAHW,MAAM,eACN,MAAM,QAIhB;IAED;;;;;;;OAOG;IACH,cAFa,OAAO,CAAC,IAAI,CAAC,CAgBzB;IAED;;;;;SAKK;IACL,6BAHa,MAAM,eACN,MAAM,QAIlB;IAED;;;;;;OAMG;IACH,cAJW,MAAM,YACN;QAAC,KAAK,CAAC,EAAE,MAAM,GAAC,eAAe,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAC,eAAe,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAC,GACvF,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAC,CAAC,CAI7F;IAED;;;;;;OAMG;IACH,yBAJW,MAAM,SACN,MAAM,GAAC,MAAM,GAAC,OAAO,GAAC,IAAI,YAC1B,OAAO,QAIjB;IAED;;;;;OAKG;IACH,mBAHW,KAAK,GAAC,MAAM,qBACZ,MAAM,QAIhB;IAED;;;;;OAKG;IACH,iBAHW,MAAM,GAAC,IAAI,iBACX,OAAO,QAIjB;IAED;;;;;;;OAOG;IACH,6BAJW,MAAM,GAAC,IAAI,QAMrB;IAED;;;;;MAKE;IACF,aAHW,MAAM,YACN;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,GAAC,OAAO,GAAC,OAAO,GAAC,MAAM,GAAC,MAAM,CAAA;KAAC,QAIpF;;CACF;0BA9cY,OAAO,mCAAmC,EAAE,WAAW;kCACvD,OAAO,mCAAmC,EAAE,mBAAmB;qCAC/D,OAAO,mCAAmC,EAAE,sBAAsB"}
1
+ {"version":3,"file":"registered-iframe-entity.d.ts","sourceRoot":"","sources":["../../../src/interfaces/registered-iframe-entity.js"],"names":[],"mappings":"AAuBA;;;;GAIG;AAEH;;;;;;GAMG;AACH;IAkDE;;;OAGG;IACH,kBAFW,sBAAsB,EAyDhC;IA5GD,kCAAkC;IAClC,UADW,mBAAmB,CAW7B;IAyBD,mBANa,OAAO,EAQnB;IAbD;;;;;;OAMG;IACH,eAFa,OAAO,CAInB;IAuTD;;;;;OAKG;IACH,oBAHW,MAAM,eACN,MAAM,QAIhB;IAED;;;;;;;OAOG;IACH,cAFa,OAAO,CAAC,IAAI,CAAC,CAgBzB;IAED;;;;;SAKK;IACL,6BAHa,MAAM,eACN,MAAM,QAIlB;IAED;;;;;;OAMG;IACH,cAJW,MAAM,YACN;QAAC,KAAK,CAAC,EAAE,MAAM,GAAC,eAAe,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAC,eAAe,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAC,GACvF,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAC,CAAC,CAI7F;IAED;;;;;;OAMG;IACH,yBAJW,MAAM,SACN,MAAM,GAAC,MAAM,GAAC,OAAO,GAAC,IAAI,YAC1B,OAAO,QAIjB;IAED;;;;;OAKG;IACH,mBAHW,KAAK,GAAC,MAAM,qBACZ,MAAM,QAIhB;IAED;;;;;OAKG;IACH,iBAHW,MAAM,GAAC,IAAI,iBACX,OAAO,QAIjB;IAED;;;;;;;OAOG;IACH,6BAJW,MAAM,GAAC,IAAI,QAMrB;IAED;;;;;MAKE;IACF,aAHW,MAAM,YACN;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,GAAC,OAAO,GAAC,OAAO,GAAC,MAAM,GAAC,MAAM,CAAA;KAAC,QAIpF;;CACF;0BAjdY,OAAO,mCAAmC,EAAE,WAAW;kCACvD,OAAO,mCAAmC,EAAE,mBAAmB;qCAC/D,OAAO,mCAAmC,EAAE,sBAAsB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.323.0",
3
+ "version": "1.324.0-rc.1",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -170,7 +170,7 @@
170
170
  "runtime": {
171
171
  "name": "node",
172
172
  "onFail": "error",
173
- "version": ">=22.11.0"
173
+ "version": ">=24.14.0"
174
174
  },
175
175
  "packageManager": {
176
176
  "name": "npm",
@@ -204,6 +204,7 @@
204
204
  "cdn:webpack": "npx webpack --progress --config ./tools/webpack/index.mjs",
205
205
  "cdn:watch": "jung -r ./src -F '.*\\.test\\.js' --run -- npm run cdn:build:local",
206
206
  "test-server": "node ./tools/wdio/bin/server",
207
+ "electron:open": "node tools/local-browsers/electron/open.mjs",
207
208
  "lt:update-browsers": "node ./tools/browsers-lists/lt-update-supported.mjs",
208
209
  "lt:upload-webview-assets": "node ./tools/lambda-test/upload-webview-assets.mjs",
209
210
  "build:frameworks": "npm run build:npm && npm --prefix ./tools/test-builds run build-frameworks",
@@ -297,4 +298,4 @@
297
298
  "README.md",
298
299
  "CHANGELOG.md"
299
300
  ]
300
- }
301
+ }
@@ -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-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'
@@ -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
- output.bytes -= prop.length + stringify(target[prop]).length
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
- correlation.performance.start = Math.floor(entry.startTime)
135
- correlation.performance.end = Math.floor(entry.responseEnd)
136
- correlation.performance.value = entry
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; 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
@@ -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
- * Subscribes to late resource timing emissions for manifest assets not yet resolved against the buffered entries.
339
- * Reuses the shared page-wide scriptObserver/poSubscribers mechanism (one PerformanceObserver for all MFEs, not
340
- * one per MFE) and, unlike that observer's own correlation bookkeeping, checks every resource entry -- not just
341
- * script-like ones -- so lazy-loaded images/fonts/stylesheets resolve too.
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 {Set<import('./manifest').ParsedManifestAsset>} pending - manifest assets still unresolved
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, pending, entryState, timingMethod) {
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 = [...pending].find(asset => asset.test(entry.name))
354
- if (matched) {
355
- applyManifestEntry(timings, entry, matched, entryState, timingMethod)
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. Weight/renderBlocking always accumulate from every detected manifest asset; timing widening
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 pending = new Set(parsedManifest.assets)
403
+ const { assets } = parsedManifest
376
404
 
377
405
  const resourceEntries = globalScope.performance?.getEntriesByType('resource') || []
378
406
  resourceEntries.forEach((entry) => {
379
- const matched = [...pending].find(asset => asset.test(entry.name))
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
- if (pending.size) subscribeToLateManifestEntries(timings, pending, entryState, target.timingMethod)
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
  /**
@@ -16,7 +16,7 @@ import { canCapturePayload, isLikelyHumanReadable, parseQueryString, createStrin
16
16
  import { Obfuscator } from '../../../common/util/obfuscate'
17
17
  import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate, isMfeTarget } from '../../../common/v2/utils'
18
18
  import { EVENT_TYPES } from '../../../common/constants/events'
19
- import { generateUuid } from '../../../common/ids/unique-id'
19
+ import { generateSpanId, generateTraceId, generateUuid } from '../../../common/ids/unique-id'
20
20
 
21
21
  export class Aggregate extends AggregateBase {
22
22
  static featureName = FEATURE_NAME
@@ -139,7 +139,20 @@ export class Aggregate extends AggregateBase {
139
139
  /** make a copy of the event for the MFE target if it exists */
140
140
  if (isMfeTarget(target)) {
141
141
  this.events.add({ ...event, targetAttributes: getVersion2Attributes(target, this) })
142
- if (shouldDuplicate(target, this)) this.reportContainerEvent({ ...event, targetAttributes: getVersion2DuplicationAttributes(target, this) }, ctx)
142
+ if (shouldDuplicate(target, this)) {
143
+ const duplicateEvent = { ...event, targetAttributes: getVersion2DuplicationAttributes(target, this) }
144
+ /** the container-duplicate copy represents the same single real HTTP call as the MFE event above, so it
145
+ * needs its own independent trace/span identity -- otherwise the two would collide on the same
146
+ * spanId/traceId, and BELC could not create two separate Spans/Traces from them. Only the id namespace is
147
+ * independent; spanTimestamp is intentionally left as-is (event.spanTimestamp), since it still reflects the
148
+ * real call's actual timing. When `event.spanId` was never set (e.g. distributed tracing is disabled), there
149
+ * is no span identity to duplicate either -- both copies correctly end up with no spanId/traceId. */
150
+ if (event.spanId) {
151
+ duplicateEvent.spanId = generateSpanId()
152
+ duplicateEvent.traceId = generateTraceId()
153
+ }
154
+ this.reportContainerEvent(duplicateEvent, ctx)
155
+ }
143
156
  } else {
144
157
  this.reportContainerEvent(event, ctx)
145
158
  }
@@ -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
  }
@@ -107,11 +107,14 @@ export class RegisteredIframeEntity {
107
107
  return
108
108
  }
109
109
 
110
+ /* Script timings are found by parsing the call stack for the script that called register(), so this has to run
111
+ synchronously here. By the time the registration promise resolves, that caller is no longer on the stack. */
112
+ const timings = findScriptTimings(opts)
113
+
110
114
  // Store the registration promise so other methods can wait for it
111
115
  this.#registrationPromise = this.#register(opts)
112
116
  this.#registrationPromise
113
117
  .then(() => {
114
- const timings = findScriptTimings()
115
118
  // Send initial timing values, skipping ones still at their "not yet known" default
116
119
  // (e.g. reportedAt: undefined, fetchStart: 0) to avoid pointless postMessage calls
117
120
  for (const [key, value] of Object.entries(timings)) {