@newrelic/browser-agent 1.313.1 → 1.314.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/cjs/common/constants/env.cdn.js +1 -1
  3. package/dist/cjs/common/constants/env.npm.js +1 -1
  4. package/dist/cjs/common/dom/selector-path.js +12 -3
  5. package/dist/cjs/common/timing/time-keeper.js +18 -6
  6. package/dist/cjs/common/vitals/load-time.js +5 -2
  7. package/dist/cjs/features/ajax/aggregate/index.js +6 -2
  8. package/dist/cjs/features/ajax/constants.js +4 -3
  9. package/dist/cjs/features/generic_events/aggregate/index.js +60 -53
  10. package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  11. package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +4 -3
  12. package/dist/cjs/features/session_replay/aggregate/index.js +15 -6
  13. package/dist/cjs/features/session_replay/constants.js +1 -1
  14. package/dist/cjs/features/session_replay/shared/recorder.js +3 -1
  15. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +7 -3
  16. package/dist/esm/common/constants/env.cdn.js +1 -1
  17. package/dist/esm/common/constants/env.npm.js +1 -1
  18. package/dist/esm/common/dom/selector-path.js +13 -3
  19. package/dist/esm/common/timing/time-keeper.js +18 -6
  20. package/dist/esm/common/vitals/load-time.js +5 -2
  21. package/dist/esm/features/ajax/aggregate/index.js +7 -3
  22. package/dist/esm/features/ajax/constants.js +3 -2
  23. package/dist/esm/features/generic_events/aggregate/index.js +61 -54
  24. package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  25. package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +4 -3
  26. package/dist/esm/features/session_replay/aggregate/index.js +15 -6
  27. package/dist/esm/features/session_replay/constants.js +1 -1
  28. package/dist/esm/features/session_replay/shared/recorder.js +3 -1
  29. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +5 -1
  30. package/dist/types/common/dom/selector-path.d.ts +2 -1
  31. package/dist/types/common/dom/selector-path.d.ts.map +1 -1
  32. package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
  33. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  34. package/dist/types/features/ajax/constants.d.ts +1 -0
  35. package/dist/types/features/ajax/constants.d.ts.map +1 -1
  36. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  37. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts +1 -0
  38. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts.map +1 -1
  39. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts +2 -0
  40. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -1
  41. package/dist/types/features/session_replay/aggregate/index.d.ts +1 -11
  42. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  43. package/dist/types/features/session_replay/shared/recorder.d.ts +2 -0
  44. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  45. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +1 -0
  46. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  47. package/package.json +1 -1
  48. package/src/common/dom/selector-path.js +13 -4
  49. package/src/common/timing/time-keeper.js +17 -6
  50. package/src/common/vitals/load-time.js +5 -2
  51. package/src/features/ajax/aggregate/index.js +6 -3
  52. package/src/features/ajax/constants.js +3 -1
  53. package/src/features/generic_events/aggregate/index.js +42 -39
  54. package/src/features/generic_events/aggregate/user-actions/aggregated-user-action.js +2 -1
  55. package/src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +4 -3
  56. package/src/features/session_replay/aggregate/index.js +16 -5
  57. package/src/features/session_replay/constants.js +1 -1
  58. package/src/features/session_replay/shared/recorder.js +3 -1
  59. package/src/features/soft_navigations/aggregate/ajax-node.js +4 -1
@@ -10,12 +10,15 @@ export const loadTime = new VitalMetric(VITAL_NAMES.LOAD_TIME);
10
10
  if (isBrowserScope) {
11
11
  const perf = globalScope.performance;
12
12
  const handler = () => {
13
- if (!loadTime.isValid && perf) {
13
+ // setTimeout defers the read until after the load event handler returns,
14
+ // ensuring loadEventEnd is populated (non-zero) — matching the web-vitals onTTFB pattern
15
+ setTimeout(() => {
16
+ if (loadTime.isValid || !perf) return;
14
17
  const navEntry = getNavigationEntry();
15
18
  loadTime.update({
16
19
  value: navEntry ? navEntry.loadEventEnd : perf.timing?.loadEventEnd - originTime
17
20
  });
18
- }
21
+ }, 0);
19
22
  };
20
23
  onWindowLoad(handler, true);
21
24
  }
@@ -6,13 +6,14 @@ import { registerHandler } from '../../../common/event-emitter/register-handler'
6
6
  import { stringify } from '../../../common/util/stringify';
7
7
  import { handle } from '../../../common/event-emitter/handle';
8
8
  import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list';
9
- import { FEATURE_NAME } from '../constants';
9
+ import { AJAX_ID, FEATURE_NAME } from '../constants';
10
10
  import { FEATURE_NAMES } from '../../../loaders/features/features';
11
11
  import { AggregateBase } from '../../utils/aggregate-base';
12
12
  import { parseGQL } from './gql';
13
13
  import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer';
14
14
  import { gosNREUMOriginals } from '../../../common/window/nreum';
15
15
  import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils';
16
+ import { generateUuid } from '../../../common/ids/unique-id';
16
17
  export class Aggregate extends AggregateBase {
17
18
  static featureName = FEATURE_NAME;
18
19
  constructor(agentRef) {
@@ -80,7 +81,8 @@ export class Aggregate extends AggregateBase {
80
81
  type,
81
82
  startTime,
82
83
  endTime,
83
- callbackDuration: metrics.cbTime
84
+ callbackDuration: metrics.cbTime,
85
+ [AJAX_ID]: generateUuid() // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
84
86
  };
85
87
  if (ctx.dt) {
86
88
  event.spanId = ctx.dt.spanId;
@@ -150,7 +152,9 @@ export class Aggregate extends AggregateBase {
150
152
  const attrParts = addCustomAttributes({
151
153
  ...(jsAttributes || {}),
152
154
  ...(event.gql || {}),
153
- ...(event.targetAttributes || {}) // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
155
+ ...(event.targetAttributes || {}),
156
+ // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
157
+ [AJAX_ID]: event[AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
154
158
  }, addString);
155
159
  fields.unshift(numeric(attrParts.length));
156
160
  insert += fields.join(',');
@@ -1,6 +1,7 @@
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 { FEATURE_NAMES } from '../../loaders/features/features';
6
- export const FEATURE_NAME = FEATURE_NAMES.ajax;
6
+ export const FEATURE_NAME = FEATURE_NAMES.ajax;
7
+ export const AJAX_ID = 'ajaxRequest.id';
@@ -14,7 +14,7 @@ import { applyFnToProps } from '../../../common/util/traverse';
14
14
  import { UserActionsAggregator } from './user-actions/user-actions-aggregator';
15
15
  import { isIFrameWindow } from '../../../common/dom/iframe';
16
16
  import { isPureObject } from '../../../common/util/type-check';
17
- import { getVersion2Attributes } from '../../../common/v2/utils';
17
+ import { getVersion2Attributes, getVersion2DuplicationAttributes, shouldDuplicate } from '../../../common/v2/utils';
18
18
  export class Aggregate extends AggregateBase {
19
19
  static featureName = FEATURE_NAME;
20
20
  #userActionAggregator;
@@ -54,7 +54,7 @@ export class Aggregate extends AggregateBase {
54
54
  }
55
55
  let addUserAction = () => {/** no-op */};
56
56
  if (isBrowserScope && agentRef.init.user_actions.enabled) {
57
- this.#userActionAggregator = new UserActionsAggregator();
57
+ this.#userActionAggregator = new UserActionsAggregator(this.agentRef);
58
58
  this.harvestOpts.beforeUnload = () => addUserAction?.(this.#userActionAggregator.aggregationEvent);
59
59
  addUserAction = aggregatedUserAction => {
60
60
  try {
@@ -66,56 +66,58 @@ export class Aggregate extends AggregateBase {
66
66
  timeStamp,
67
67
  type
68
68
  } = aggregatedUserAction.event;
69
- const userActionEvent = {
70
- eventType: 'UserAction',
71
- timestamp: this.#toEpoch(timeStamp),
72
- action: type,
73
- actionCount: aggregatedUserAction.count,
74
- actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
75
- actionMs: aggregatedUserAction.relativeMs,
76
- rageClick: aggregatedUserAction.rageClick,
77
- target: aggregatedUserAction.selectorPath,
78
- currentUrl: aggregatedUserAction.currentUrl,
79
- ...(isIFrameWindow(window) && {
80
- iframe: true
81
- }),
82
- ...this.agentRef.init.user_actions.elementAttributes.reduce((acc, field) => {
83
- /** prevent us from capturing an obscenely long value */
84
- if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128);
85
- return acc;
86
- }, {}),
87
- ...aggregatedUserAction.nearestTargetFields,
88
- ...(aggregatedUserAction.deadClick && {
89
- deadClick: true
90
- }),
91
- ...(aggregatedUserAction.errorClick && {
92
- errorClick: true
93
- })
94
- };
95
- this.addEvent(userActionEvent);
96
- this.#trackUserActionSM(userActionEvent);
69
+ aggregatedUserAction.targets.forEach(mfeTarget => {
70
+ const userActionEvent = {
71
+ eventType: 'UserAction',
72
+ timestamp: this.#toEpoch(timeStamp),
73
+ action: type,
74
+ actionCount: aggregatedUserAction.count,
75
+ actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
76
+ actionMs: aggregatedUserAction.relativeMs,
77
+ rageClick: aggregatedUserAction.rageClick,
78
+ target: aggregatedUserAction.selectorPath,
79
+ currentUrl: aggregatedUserAction.currentUrl,
80
+ ...(isIFrameWindow(window) && {
81
+ iframe: true
82
+ }),
83
+ ...this.agentRef.init.user_actions.elementAttributes.reduce((acc, field) => {
84
+ /** prevent us from capturing an obscenely long value */
85
+ if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128);
86
+ return acc;
87
+ }, {}),
88
+ ...aggregatedUserAction.nearestTargetFields,
89
+ ...(aggregatedUserAction.deadClick && {
90
+ deadClick: true
91
+ }),
92
+ ...(aggregatedUserAction.errorClick && {
93
+ errorClick: true
94
+ })
95
+ };
96
+ this.addEvent(userActionEvent, mfeTarget);
97
+ this.#trackUserActionSM(userActionEvent);
97
98
 
98
- /**
99
- * Returns the original target field name with `target` prepended and camelCased
100
- * @param {string} originalFieldName
101
- * @returns {string} the target field name
102
- */
103
- function targetAttrName(originalFieldName) {
104
- /** preserve original renaming structure for pre-existing field maps */
105
- if (originalFieldName === 'tagName') originalFieldName = 'tag';
106
- if (originalFieldName === 'className') originalFieldName = 'class';
107
- /** return the original field name, cap'd and prepended with target to match formatting */
108
- return "target".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
109
- }
99
+ /**
100
+ * Returns the original target field name with `target` prepended and camelCased
101
+ * @param {string} originalFieldName
102
+ * @returns {string} the target field name
103
+ */
104
+ function targetAttrName(originalFieldName) {
105
+ /** preserve original renaming structure for pre-existing field maps */
106
+ if (originalFieldName === 'tagName') originalFieldName = 'tag';
107
+ if (originalFieldName === 'className') originalFieldName = 'class';
108
+ /** return the original field name, cap'd and prepended with target to match formatting */
109
+ return "target".concat(originalFieldName.charAt(0).toUpperCase() + originalFieldName.slice(1));
110
+ }
110
111
 
111
- /**
112
- * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
113
- * @param {string} attribute The attribute to check for on the target element
114
- * @returns {boolean} Whether the target element has the attribute and can be trusted
115
- */
116
- function canTrustTargetAttribute(attribute) {
117
- return !!(aggregatedUserAction.selectorPath !== 'window' && aggregatedUserAction.selectorPath !== 'document' && target instanceof HTMLElement && target?.[attribute]);
118
- }
112
+ /**
113
+ * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
114
+ * @param {string} attribute The attribute to check for on the target element
115
+ * @returns {boolean} Whether the target element has the attribute and can be trusted
116
+ */
117
+ function canTrustTargetAttribute(attribute) {
118
+ return !!(aggregatedUserAction.selectorPath !== 'window' && aggregatedUserAction.selectorPath !== 'document' && target instanceof HTMLElement && target?.[attribute]);
119
+ }
120
+ });
119
121
  }
120
122
  } catch (e) {
121
123
  // do nothing for now
@@ -315,9 +317,7 @@ export class Aggregate extends AggregateBase {
315
317
  timestamp: this.#toEpoch(now()),
316
318
  /** all generic events require pageUrl(s) */
317
319
  pageUrl: cleanURL('' + initialLocation),
318
- currentUrl: cleanURL('' + location),
319
- /** Specific attributes only supplied if harvesting to endpoint version 2 */
320
- ...getVersion2Attributes(target, this)
320
+ currentUrl: cleanURL('' + location)
321
321
  };
322
322
  const eventAttributes = {
323
323
  /** Agent-level custom attributes */
@@ -327,7 +327,14 @@ export class Aggregate extends AggregateBase {
327
327
  /** Event-specific attributes take precedence over agent-level custom attributes and fallbacks */
328
328
  ...obj
329
329
  };
330
- this.events.add(eventAttributes);
330
+ this.events.add({
331
+ ...eventAttributes,
332
+ ...getVersion2Attributes(target, this)
333
+ });
334
+ if (shouldDuplicate(target, this)) this.addEvent({
335
+ ...eventAttributes,
336
+ ...getVersion2DuplicationAttributes(target, this)
337
+ });
331
338
  }
332
339
  serializer(eventBuffer) {
333
340
  return applyFnToProps({
@@ -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 { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../constants';
@@ -16,6 +16,7 @@ export class AggregatedUserAction {
16
16
  this.currentUrl = cleanURL('' + location);
17
17
  this.deadClick = false;
18
18
  this.errorClick = false;
19
+ this.targets = selectorInfo.targets;
19
20
  }
20
21
 
21
22
  /**
@@ -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 { analyzeElemPath } from '../../../../common/dom/selector-path';
@@ -14,7 +14,8 @@ export class UserActionsAggregator {
14
14
  #deadClickTimer = undefined;
15
15
  #domObserver = undefined;
16
16
  #errorClickTimer = undefined;
17
- constructor() {
17
+ constructor(agentRef) {
18
+ this.agentRef = agentRef;
18
19
  if (gosNREUMOriginals().o.MO) {
19
20
  this.#domObserver = new MutationObserver(this.isLiveClick.bind(this));
20
21
  }
@@ -37,7 +38,7 @@ export class UserActionsAggregator {
37
38
  process(evt, targetFields) {
38
39
  if (!evt) return;
39
40
  const targetElem = OBSERVED_WINDOW_EVENTS.includes(evt.type) ? window : evt.target;
40
- const selectorInfo = analyzeElemPath(targetElem, targetFields);
41
+ const selectorInfo = analyzeElemPath(targetElem, targetFields, this.agentRef);
41
42
 
42
43
  // if selectorInfo.path is undefined, aggregation will be skipped for this event
43
44
  const aggregationKey = getAggregationKey(evt, selectorInfo.path);
@@ -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
  /**
@@ -215,6 +215,7 @@ export class Aggregate extends AggregateBase {
215
215
  }
216
216
  }
217
217
  makeHarvestPayload() {
218
+ if (this.isRetrying) return this.recorder.retryPayload;
218
219
  if (this.mode !== MODE.FULL || this.blocked) return; // harvests should only be made in FULL mode, and not if the feature is blocked
219
220
  if (this.shouldCompress && !this.gzipper) return; // if compression is enabled, but the libraries have not loaded, wait for them to load
220
221
  if (!this.recorder || !this.timeKeeper?.ready || !(this.recorder.hasSeenSnapshot && this.recorder.hasSeenMeta)) return; // if the recorder or the timekeeper is not ready, or the recorder has not yet seen a snapshot, do not harvest
@@ -242,8 +243,6 @@ export class Aggregate extends AggregateBase {
242
243
  this.abort(ABORT_REASONS.TOO_BIG, len);
243
244
  return;
244
245
  }
245
-
246
- // TODO -- Gracefully handle the buffer for retries.
247
246
  if (!this.agentRef.runtime.session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({
248
247
  sessionReplaySentFirstChunk: true
249
248
  });
@@ -251,6 +250,7 @@ export class Aggregate extends AggregateBase {
251
250
  if (!this.agentRef.runtime.session.state.traceHarvestStarted) {
252
251
  warn(59, JSON.stringify(this.agentRef.runtime.session.state));
253
252
  }
253
+ this.recorder.retryPayload = payload;
254
254
  return payload;
255
255
  }
256
256
 
@@ -348,9 +348,18 @@ export class Aggregate extends AggregateBase {
348
348
  };
349
349
  }
350
350
  postHarvestCleanup(result) {
351
- // The mutual decision for now is to stop recording and clear buffers if ingest is experiencing 429 rate limiting
352
- if (result.status === 429) {
353
- this.abort(ABORT_REASONS.TOO_MANY);
351
+ if (result.sent) {
352
+ if (result.retry) {
353
+ warn(70);
354
+ this.isRetrying = true;
355
+ this.forceStop();
356
+ } else {
357
+ this.recorder.retryPayload = undefined;
358
+ if (this.isRetrying) {
359
+ this.isRetrying = false;
360
+ this.switchToFull();
361
+ }
362
+ }
354
363
  }
355
364
  }
356
365
 
@@ -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 { MODE } from '../../common/session/constants';
@@ -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 { record as recorder } from '@newrelic/rrweb';
@@ -40,6 +40,8 @@ export class Recorder {
40
40
  this.events = new RecorderEvents(this.shouldFix);
41
41
  /** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
42
42
  this.backloggedEvents = new RecorderEvents(this.shouldFix);
43
+ /** Used to hold the harvest contents to facilitate retrying */
44
+ this.retryPayload = undefined;
43
45
  /** Only set to true once a snapshot node has been processed. Used to block harvests from sending before we know we have a snapshot */
44
46
  this.hasSeenSnapshot = false;
45
47
  this.hasSeenMeta = false;
@@ -3,6 +3,7 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer';
6
+ import { AJAX_ID } from '../../ajax/constants';
6
7
  import { NODE_TYPE } from '../constants';
7
8
  import { BelNode } from './bel-node';
8
9
  export class AjaxNode extends BelNode {
@@ -21,6 +22,8 @@ export class AjaxNode extends BelNode {
21
22
  this.spanTimestamp = ajaxEvent.spanTimestamp;
22
23
  this.gql = ajaxEvent.gql;
23
24
  this.targetAttributes = ajaxEvent.targetAttributes;
25
+ this[AJAX_ID] = ajaxEvent[AJAX_ID]; // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
26
+
24
27
  this.start = ajaxEvent.startTime;
25
28
  this.end = ajaxEvent.endTime;
26
29
  if (ajaxContext?.latestLongtaskEnd) {
@@ -46,7 +49,8 @@ export class AjaxNode extends BelNode {
46
49
  addString(this.method), numeric(this.status), addString(this.domain), addString(this.path), numeric(this.txSize), numeric(this.rxSize), this.requestedWith, addString(this.nodeId), nullable(this.spanId, addString, true) + nullable(this.traceId, addString, true) + nullable(this.spanTimestamp, numeric)];
47
50
  let allAttachedNodes = addCustomAttributes({
48
51
  ...(this.gql || {}),
49
- ...(this.targetAttributes || {})
52
+ ...(this.targetAttributes || {}),
53
+ [AJAX_ID]: this[AJAX_ID]
50
54
  }, addString);
51
55
  this.children.forEach(node => allAttachedNodes.push(node.serialize())); // no children is expected under ajax nodes at this time
52
56
 
@@ -1,6 +1,7 @@
1
- export function analyzeElemPath(elem: HTMLElement, targetFields?: Array<string>): {
1
+ export function analyzeElemPath(elem: HTMLElement, targetFields?: Array<string>, agentRef: any): {
2
2
  path: (undefined | string);
3
3
  nearestFields: {};
4
+ targets: any[];
4
5
  hasButton: boolean;
5
6
  hasLink: boolean;
6
7
  };
@@ -1 +1 @@
1
- {"version":3,"file":"selector-path.d.ts","sourceRoot":"","sources":["../../../../src/common/dom/selector-path.js"],"names":[],"mappings":"AAgBO,sCAJI,WAAW,iBACX,KAAK,CAAC,MAAM,CAAC,GACX;IAAC,IAAI,EAAE,CAAC,SAAS,GAAC,MAAM,CAAC,CAAC;IAAC,aAAa,EAAE,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAC,CA2B/F"}
1
+ {"version":3,"file":"selector-path.d.ts","sourceRoot":"","sources":["../../../../src/common/dom/selector-path.js"],"names":[],"mappings":"AAkBO,sCAJI,WAAW,iBACX,KAAK,CAAC,MAAM,CAAC,kBACX;IAAC,IAAI,EAAE,CAAC,SAAS,GAAC,MAAM,CAAC,CAAC;IAAC,aAAa,EAAE,EAAE,CAAC;IAAC,OAAO,QAAQ;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAC,CAkC/G"}
@@ -1 +1 @@
1
- {"version":3,"file":"time-keeper.d.ts","sourceRoot":"","sources":["../../../../src/common/timing/time-keeper.js"],"names":[],"mappings":"AAUA;;;;GAIG;AACH;IA6BE,6BAIC;IAsBD,qBAEC;IAED,kCAEC;IAED,4BAEC;IAED;;;;;;OAMG;IACH,8BALsB,cAAc,aACf,MAAM,WACR,MAAM,gBACD,MAAM,QAqB7B;IAED;;;;;OAKG;IACH,uCAHwB,MAAM,GACjB,MAAM,CAKlB;IAED;;;;;OAKG;IACH,0CAFa,MAAM,CAKlB;IAED;;;;OAIG;IACH,oCAHqB,MAAM,GACf,MAAM,CAKjB;IAED;;;;OAIG;IACH,uCAHW,mBAAmB,GACjB,MAAM,CAKlB;IAED,+FAA+F;IAC/F,0BASC;;CACF"}
1
+ {"version":3,"file":"time-keeper.d.ts","sourceRoot":"","sources":["../../../../src/common/timing/time-keeper.js"],"names":[],"mappings":"AAUA;;;;GAIG;AACH;IAkCE,6BAIC;IA0BD,qBAEC;IAED,kCAEC;IAED,4BAEC;IAED;;;;;;OAMG;IACH,8BALsB,cAAc,aACf,MAAM,WACR,MAAM,gBACD,MAAM,QAqB7B;IAED;;;;;OAKG;IACH,uCAHwB,MAAM,GACjB,MAAM,CAMlB;IAED;;;;;OAKG;IACH,0CAFa,MAAM,CAMlB;IAED;;;;OAIG;IACH,oCAHqB,MAAM,GACf,MAAM,CAKjB;IAED;;;;OAIG;IACH,uCAHW,mBAAmB,GACjB,MAAM,CAKlB;IAED,+FAA+F;IAC/F,0BASC;;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/ajax/aggregate/index.js"],"names":[],"mappings":"AAgBA;IACE,2BAAiC;IAEjC,2BA0BC;IAED,0GAuEC;IAED,+CAOC;IAED,iDAwDC;CACF;8BAhL6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/ajax/aggregate/index.js"],"names":[],"mappings":"AAiBA;IACE,2BAAiC;IAEjC,2BA0BC;IAED,0GAwEC;IAED,+CAOC;IAED,iDAyDC;CACF;8BAnL6B,4BAA4B"}
@@ -1,2 +1,3 @@
1
1
  export const FEATURE_NAME: string;
2
+ export const AJAX_ID: "ajaxRequest.id";
2
3
  //# sourceMappingURL=constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/ajax/constants.js"],"names":[],"mappings":"AAMA,kCAA8C"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/ajax/constants.js"],"names":[],"mappings":"AAMA,kCAA8C;AAE9C,sBAAuB,gBAAgB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAAiC;IAGjC,2BAqQC;IAnQC,gCAAkG;IAsQpG;;;;;;;;;;;;OAYG;IACH,eAJW,MAAM,YAAC,WACP,MAAM,YAAC,QAmCjB;IAED,qCAEC;IAED;;;MAEC;;CAsBF;8BAjW6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAAiC;IAGjC,2BAyQC;IAvQC,gCAAkG;IA0QpG;;;;;;;;;;;;OAYG;IACH,eAJW,MAAM,YAAC,WACP,MAAM,YAAC,QAkCjB;IAED,qCAEC;IAED;;;MAEC;;CAsBF;8BApW6B,4BAA4B"}
@@ -10,6 +10,7 @@ export class AggregatedUserAction {
10
10
  currentUrl: string;
11
11
  deadClick: boolean;
12
12
  errorClick: boolean;
13
+ targets: any;
13
14
  /**
14
15
  * Aggregates the count and maintains the relative MS array for matching events
15
16
  * Will determine if a rage click was observed as part of the aggregation
@@ -1 +1 @@
1
- {"version":3,"file":"aggregated-user-action.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/aggregated-user-action.js"],"names":[],"mappings":"AAOA;IACE,yCAWC;IAVC,WAAgB;IAChB,cAAc;IACd,iBAAyC;IACzC,qBAAqB;IACrB,kBAAqC;IACrC,+BAA0B;IAC1B,yBAAqD;IACrD,mBAAyC;IACzC,mBAAsB;IACtB,oBAAuB;IAGzB;;;;;OAKG;IACH,eAHW,KAAK,GACH,IAAI,CAMhB;IAED;;;OAGG;IACH,eAFa,OAAO,CAKnB;CACF"}
1
+ {"version":3,"file":"aggregated-user-action.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/aggregated-user-action.js"],"names":[],"mappings":"AAOA;IACE,yCAYC;IAXC,WAAgB;IAChB,cAAc;IACd,iBAAyC;IACzC,qBAAqB;IACrB,kBAAqC;IACrC,+BAA0B;IAC1B,yBAAqD;IACrD,mBAAyC;IACzC,mBAAsB;IACtB,oBAAuB;IACvB,aAAmC;IAGrC;;;;;OAKG;IACH,eAHW,KAAK,GACH,IAAI,CAMhB;IAED;;;OAGG;IACH,eAFa,OAAO,CAKnB;CACF"}
@@ -1,4 +1,6 @@
1
1
  export class UserActionsAggregator {
2
+ constructor(agentRef: any);
3
+ agentRef: any;
2
4
  get aggregationEvent(): AggregatedUserAction | undefined;
3
5
  /**
4
6
  * Process the event and determine if a new aggregation set should be made or if it should increment the current aggregation
@@ -1 +1 @@
1
- {"version":3,"file":"user-actions-aggregator.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js"],"names":[],"mappings":"AAUA;IAcE,yDAQC;IAED;;;;OAIG;IACH,aAHW,KAAK,sBACH,oBAAoB,GAAC,SAAS,CA2B1C;IAED,yBAKC;IA0CD,oBAEC;;CACF;qCA9GoC,0BAA0B"}
1
+ {"version":3,"file":"user-actions-aggregator.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js"],"names":[],"mappings":"AAUA;IAQE,2BAKC;IAJC,cAAwB;IAM1B,yDAQC;IAED;;;;OAIG;IACH,aAHW,KAAK,sBACH,oBAAoB,GAAC,SAAS,CA2B1C;IAED,yBAKC;IA0CD,oBAEC;;CACF;qCA/GoC,0BAA0B"}
@@ -35,17 +35,7 @@ export class Aggregate extends AggregateBase {
35
35
  PRELOAD: string;
36
36
  }): void;
37
37
  prepUtils(): Promise<void>;
38
- makeHarvestPayload(): {
39
- qs: {
40
- browser_monitoring_key: any;
41
- type: string;
42
- app_id: any;
43
- protocol_version: string;
44
- timestamp: any;
45
- attributes: string;
46
- };
47
- body: any;
48
- } | undefined;
38
+ makeHarvestPayload(): any;
49
39
  /**
50
40
  * returns the timestamps for the earliest and latest nodes in the provided array, even if out of order
51
41
  * @param {Object[]} [nodes] - the nodes to evaluate
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AAyBA;IACE,2BAAiC;IAIjC,sCAyFC;IA5FD,aAAe;IAKb,iFAAiF;IACjF,qBAAwB;IAGxB,2CAA2C;IAC3C,sDAAwB;IACxB,6CAA6C;IAC7C,gDAAmB;IACnB,+DAA+D;IAC/D,wBAA0B;IAE1B,0BAA0B;IAC1B,kBAAqB;IACrB,6CAA6C;IAC7C,gBAA2B;IAE3B,qBAA2B;IAE3B,cAA8C;IAI9C,kCAAqG;IAmEvG,0BAEC;IAED,0BAMC;IAED,qBAUC;IAED;;;;;;OAMG;IACH,4BALW,OAAO,iBACP,OAAO;;;;;;QAEL,IAAI,CA8ChB;IAED,2BAUC;IAED;;;;;;;;;;kBAwCC;IAED;;;;OAIG;IACH,6BAHW,MAAM,EAAE,GACN;QAAE,UAAU,EAAE,MAAM,GAAC,SAAS,CAAC;QAAC,SAAS,EAAE,MAAM,GAAC,SAAS,CAAA;KAAE,CAUzE;IAED;;;;;;;;;;MAsEC;IAED,sCAKC;IAED;;;;OAIG;IACH,mCAKC;IAED,yDAAyD;IACzD,+CASC;IAED,yCAIC;CACF;8BA7W6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AAyBA;IACE,2BAAiC;IAIjC,sCAyFC;IA5FD,aAAe;IAKb,iFAAiF;IACjF,qBAAwB;IAGxB,2CAA2C;IAC3C,sDAAwB;IACxB,6CAA6C;IAC7C,gDAAmB;IACnB,+DAA+D;IAC/D,wBAA0B;IAE1B,0BAA0B;IAC1B,kBAAqB;IACrB,6CAA6C;IAC7C,gBAA2B;IAE3B,qBAA2B;IAE3B,cAA8C;IAI9C,kCAAqG;IAmEvG,0BAEC;IAED,0BAMC;IAED,qBAUC;IAED;;;;;;OAMG;IACH,4BALW,OAAO,iBACP,OAAO;;;;;;QAEL,IAAI,CA8ChB;IAED,2BAUC;IAED,0BA0CC;IAED;;;;OAIG;IACH,6BAHW,MAAM,EAAE,GACN;QAAE,UAAU,EAAE,MAAM,GAAC,SAAS,CAAC;QAAC,SAAS,EAAE,MAAM,GAAC,SAAS,CAAA;KAAE,CAUzE;IAED;;;;;;;;;;MAsEC;IAED,sCAcC;IAED;;;;OAIG;IACH,mCAKC;IAED,yDAAyD;IACzD,+CASC;IAED,yCAIC;CACF;8BAxX6B,4BAA4B"}
@@ -13,6 +13,8 @@ export class Recorder {
13
13
  events: RecorderEvents;
14
14
  /** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
15
15
  backloggedEvents: RecorderEvents;
16
+ /** Used to hold the harvest contents to facilitate retrying */
17
+ retryPayload: any;
16
18
  /** Only set to true once a snapshot node has been processed. Used to block harvests from sending before we know we have a snapshot */
17
19
  hasSeenSnapshot: boolean;
18
20
  hasSeenMeta: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAqBA;IAUE,+BAkCC;IApCD,sBAAmB;IAGjB,iDAAiD;IACjD,kBAAgC;IAEhC,QAAyB;IACzB,mBAA6C;IAC7C,cAAqC;IAErC,qBAAwB;IACxB,0FAA0F;IAC1F,eAAkE;IAElE,iHAAiH;IACjH,uBAAgD;IAChD,mFAAmF;IACnF,iCAA0D;IAC1D,uIAAuI;IACvI,yBAA4B;IAC5B,qBAAwB;IACxB,kIAAkI;IAClI,kBAAqB;IACrB,uIAAuI;IACvI,0BAAwE;IAc1E,mBAEC;IAED;;;;;;;;;MAWC;IAED,kFAAkF;IAClF,oBAGC;IAED,qDAAqD;IACrD,8CA0CC;IAED;;;;;OAKG;IACH,aAHW,GAAC,cACD,GAAC,QAiCX;IAED,yHAAyH;IACzH,yCAiCC;IAED,0HAA0H;IAC1H,yBAOC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED;;;SAGK;IACL,oCAGC;;CACF;+BAvO8B,mBAAmB"}
1
+ {"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAqBA;IAUE,+BAoCC;IAtCD,sBAAmB;IAGjB,iDAAiD;IACjD,kBAAgC;IAEhC,QAAyB;IACzB,mBAA6C;IAC7C,cAAqC;IAErC,qBAAwB;IACxB,0FAA0F;IAC1F,eAAkE;IAElE,iHAAiH;IACjH,uBAAgD;IAChD,mFAAmF;IACnF,iCAA0D;IAC1D,+DAA+D;IAC/D,kBAA6B;IAC7B,uIAAuI;IACvI,yBAA4B;IAC5B,qBAAwB;IACxB,kIAAkI;IAClI,kBAAqB;IACrB,uIAAuI;IACvI,0BAAwE;IAc1E,mBAEC;IAED;;;;;;;;;MAWC;IAED,kFAAkF;IAClF,oBAGC;IAED,qDAAqD;IACrD,8CA0CC;IAED;;;;;OAKG;IACH,aAHW,GAAC,cACD,GAAC,QAiCX;IAED,yHAAyH;IACzH,yCAiCC;IAED,0HAA0H;IAC1H,yBAOC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED;;;SAGK;IACL,oCAGC;;CACF;+BAzO8B,mBAAmB"}
@@ -15,6 +15,7 @@ export class AjaxNode extends BelNode {
15
15
  targetAttributes: any;
16
16
  callbackEnd: any;
17
17
  serialize(parentStartTimestamp: any, agentRef: any): string;
18
+ "ajaxRequest.id": any;
18
19
  }
19
20
  import { BelNode } from './bel-node';
20
21
  //# sourceMappingURL=ajax-node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ajax-node.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/ajax-node.js"],"names":[],"mappings":"AAQA;IACE,8CAsBC;IApBC,gBAA6B;IAC7B,YAA8B;IAC9B,YAA8B;IAC9B,YAA8B;IAC9B,UAA0B;IAC1B,YAAmC;IACnC,YAAoC;IACpC,+BAAwD;IACxD,YAA8B;IAC9B,aAAgC;IAChC,mBAA4C;IAC5C,SAAwB;IACxB,sBAAkD;IAKhD,iBAAoE;IAKxE,4DAiCC;CACF;wBA7DuB,YAAY"}
1
+ {"version":3,"file":"ajax-node.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/ajax-node.js"],"names":[],"mappings":"AASA;IACE,8CAuBC;IArBC,gBAA6B;IAC7B,YAA8B;IAC9B,YAA8B;IAC9B,YAA8B;IAC9B,UAA0B;IAC1B,YAAmC;IACnC,YAAoC;IACpC,+BAAwD;IACxD,YAA8B;IAC9B,aAAgC;IAChC,mBAA4C;IAC5C,SAAwB;IACxB,sBAAkD;IAMhD,iBAAoE;IAKxE,4DAkCC;IA5CC,sBAAkC;CA6CrC;wBA/DuB,YAAY"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.313.1",
3
+ "version": "1.314.0",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -1,8 +1,10 @@
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
 
6
+ import { getRegisteredTargetsFromId } from '../v2/utils'
7
+
6
8
  /**
7
9
  * Generates a CSS selector path for the given element, if possible.
8
10
  * Also gather metadata about the element's nearest fields, and whether there are any links or buttons in the path.
@@ -12,10 +14,11 @@
12
14
  *
13
15
  * @param {HTMLElement} elem
14
16
  * @param {Array<string>} [targetFields=[]] specifies which fields to gather from the nearest element in the path
15
- * @returns {{path: (undefined|string), nearestFields: {}, hasButton: boolean, hasLink: boolean}}
17
+ * @returns {{path: (undefined|string), nearestFields: {}, targets: Array, hasButton: boolean, hasLink: boolean}}
16
18
  */
17
- export const analyzeElemPath = (elem, targetFields = []) => {
18
- const result = { path: undefined, nearestFields: {}, hasButton: false, hasLink: false }
19
+ export const analyzeElemPath = (elem, targetFields = [], agentRef) => {
20
+ const targets = []
21
+ const result = { path: undefined, nearestFields: {}, get targets () { return targets.length ? targets : [undefined] }, hasButton: false, hasLink: false }
19
22
  if (!elem) return result
20
23
  if (elem === window) { result.path = 'window'; return result }
21
24
  if (elem === document) { result.path = 'document'; return result }
@@ -30,6 +33,12 @@ export const analyzeElemPath = (elem, targetFields = []) => {
30
33
  result.hasButton ||= tagName === 'button' || (tagName === 'input' && elem.type.toLowerCase() === 'button')
31
34
 
32
35
  targetFields.forEach(field => { result.nearestFields[nearestAttrName(field)] ||= (elem[field]?.baseVal || elem[field]) })
36
+
37
+ const dataAttrs = elem?.dataset
38
+ if (dataAttrs.nrMfeId) {
39
+ targets.push(...getRegisteredTargetsFromId(dataAttrs.nrMfeId, agentRef))
40
+ }
41
+
33
42
  pathSelector = buildPathSelector(elem, pathSelector)
34
43
  elem = elem.parentNode
35
44
  }
@@ -40,7 +40,12 @@ export class TimeKeeper {
40
40
  */
41
41
  #ready = false
42
42
 
43
- #reportedDrift = false
43
+ /**
44
+ * The total measured drift in milliseconds. Represents how much performance.now()
45
+ * has fallen behind Date.now(), which is used to correct timestamp conversions.
46
+ * @type {number}
47
+ */
48
+ #measuredDrift = 0
44
49
 
45
50
  constructor (sessionObj) {
46
51
  this.#session = sessionObj
@@ -49,7 +54,6 @@ export class TimeKeeper {
49
54
  }
50
55
 
51
56
  #detectDrift () {
52
- if (this.#reportedDrift) return
53
57
  try {
54
58
  // Drift detection: measures if performance.now() and Date.now() have become desynchronized
55
59
  // This can happen when a machine sleeps and the performance timer freezes while Date continues
@@ -60,8 +64,13 @@ export class TimeKeeper {
60
64
  // Note: localTimeDiff (server time offset) is NOT part of drift - that's a legitimate offset
61
65
  const drift = (Date.now() - originTime) - performance.now()
62
66
  if (drift > 1000) {
63
- this.#reportedDrift = true
64
- handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/ClockDrift/Detected', drift], undefined, FEATURE_NAMES.metrics, this.#session.agentRef.ee)
67
+ // Check if this is new drift (increase of >1000ms from last measurement)
68
+ const newDrift = drift - this.#measuredDrift
69
+ if (newDrift > 1000) {
70
+ // Update measured drift and report it
71
+ this.#measuredDrift = drift
72
+ if (this.#session) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/ClockDrift/Detected', drift], undefined, FEATURE_NAMES.metrics, this.#session.agentRef.ee)
73
+ }
65
74
  }
66
75
  } catch (err) {
67
76
  // Silently ignore drift detection errors to avoid breaking normal operation
@@ -116,7 +125,8 @@ export class TimeKeeper {
116
125
  */
117
126
  convertRelativeTimestamp (relativeTime) {
118
127
  this.#detectDrift()
119
- return originTime + relativeTime
128
+ // Add measured drift to compensate for performance.now() falling behind
129
+ return originTime + relativeTime + this.#measuredDrift
120
130
  }
121
131
 
122
132
  /**
@@ -127,7 +137,8 @@ export class TimeKeeper {
127
137
  */
128
138
  convertAbsoluteTimestamp (timestamp) {
129
139
  this.#detectDrift()
130
- return timestamp - originTime
140
+ // Subtract measured drift since we're converting from absolute to relative
141
+ return timestamp - originTime - this.#measuredDrift
131
142
  }
132
143
 
133
144
  /**
@@ -12,12 +12,15 @@ export const loadTime = new VitalMetric(VITAL_NAMES.LOAD_TIME)
12
12
  if (isBrowserScope) {
13
13
  const perf = globalScope.performance
14
14
  const handler = () => {
15
- if (!loadTime.isValid && perf) {
15
+ // setTimeout defers the read until after the load event handler returns,
16
+ // ensuring loadEventEnd is populated (non-zero) — matching the web-vitals onTTFB pattern
17
+ setTimeout(() => {
18
+ if (loadTime.isValid || !perf) return
16
19
  const navEntry = getNavigationEntry()
17
20
  loadTime.update({
18
21
  value: navEntry ? navEntry.loadEventEnd : (perf.timing?.loadEventEnd - originTime)
19
22
  })
20
- }
23
+ }, 0)
21
24
  }
22
25
 
23
26
  onWindowLoad(handler, true)