@newrelic/browser-agent 1.322.0 → 1.323.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 (31) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +21 -0
  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/harvest/connector.js +2 -0
  6. package/dist/cjs/features/ajax/aggregate/index.js +7 -1
  7. package/dist/cjs/features/ajax/instrument/distributed-tracing.js +12 -4
  8. package/dist/cjs/features/session_replay/aggregate/index.js +8 -3
  9. package/dist/cjs/features/session_trace/aggregate/index.js +6 -6
  10. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +12 -5
  11. package/dist/esm/common/constants/env.cdn.js +1 -1
  12. package/dist/esm/common/constants/env.npm.js +1 -1
  13. package/dist/esm/common/harvest/connector.js +2 -0
  14. package/dist/esm/features/ajax/aggregate/index.js +7 -1
  15. package/dist/esm/features/ajax/instrument/distributed-tracing.js +12 -4
  16. package/dist/esm/features/session_replay/aggregate/index.js +8 -3
  17. package/dist/esm/features/session_trace/aggregate/index.js +6 -6
  18. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +7 -0
  19. package/dist/types/common/harvest/connector.d.ts.map +1 -1
  20. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  21. package/dist/types/features/ajax/instrument/distributed-tracing.d.ts +1 -0
  22. package/dist/types/features/ajax/instrument/distributed-tracing.d.ts.map +1 -1
  23. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  24. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  25. package/package.json +4 -1
  26. package/src/common/harvest/connector.js +2 -0
  27. package/src/features/ajax/aggregate/index.js +7 -3
  28. package/src/features/ajax/instrument/distributed-tracing.js +12 -4
  29. package/src/features/session_replay/aggregate/index.js +6 -1
  30. package/src/features/session_trace/aggregate/index.js +6 -6
  31. package/src/features/soft_navigations/aggregate/ajax-node.js +7 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
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.323.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.322.0...v1.323.0) (2026-09-30)
7
+
8
+
9
+ ### Features
10
+
11
+ * Add internal adoption metrics for AJAX Payloads ([#1842](https://github.com/newrelic/newrelic-browser-agent/issues/1842)) ([055b917](https://github.com/newrelic/newrelic-browser-agent/commit/055b9171ef6393da18ee64cc156c51af4ac1498e))
12
+ * Harvest SR on switch from ERROR to FULL mode ([#1864](https://github.com/newrelic/newrelic-browser-agent/issues/1864)) ([b95f9cd](https://github.com/newrelic/newrelic-browser-agent/commit/b95f9cd065225ae4386a75bfa5e7acc4adb528d9))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **ajax:** correct DT header timestamp to server time ([#1867](https://github.com/newrelic/newrelic-browser-agent/issues/1867)) ([5e86506](https://github.com/newrelic/newrelic-browser-agent/commit/5e865062e5162acc7c4ee2924cf7b1db9d8f5685))
18
+ * Ensure timeKeeper readiness on Connector race condition ([#1875](https://github.com/newrelic/newrelic-browser-agent/issues/1875)) ([45de2d8](https://github.com/newrelic/newrelic-browser-agent/commit/45de2d85c730a0dd189ed6f14c7166e89ef3e1a0))
19
+
6
20
  ## [1.322.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.321.0...v1.322.0) (2026-09-11)
7
21
 
8
22
 
package/README.md CHANGED
@@ -142,6 +142,27 @@ const options = {
142
142
  }
143
143
  ```
144
144
 
145
+ ## AJAX deny list
146
+
147
+ The browser agent can be configured to exclude specific domains or paths from `AjaxRequest` event tracking using the `ajax.deny_list` setting in the `init` configuration.
148
+
149
+ ```javascript
150
+ const options = {
151
+ info: { ... },
152
+ loader_config: { ... },
153
+ init: {
154
+ ajax: {
155
+ deny_list: ["mywebsite.com/photos"]
156
+ },
157
+ ...
158
+ }
159
+ }
160
+ ```
161
+
162
+ <b>Important:</b> Updating the deny list via the app settings page or NerdGraph only updates the `NREUM.init` configuration shown in the copy/paste snippet on the app's *Application settings* page. It doesn't take effect automatically for an agent installed via NPM. If you configure the deny list using one of those methods, you must manually copy over the updated configuration, then rebuild and redeploy your application for the change to take effect.
163
+
164
+ See [Configure AjaxRequest events](https://docs.newrelic.com/docs/browser/new-relic-browser/configuration/configure-ajax-request-events/) for more information.
165
+
145
166
  ## Options Parameter
146
167
 
147
168
  The `options` parameter used, or passed in, when instantiating the `BrowserAgent` class can include the following arguments:
@@ -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.322.0";
20
+ const VERSION = exports.VERSION = "1.323.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.322.0";
20
+ const VERSION = exports.VERSION = "1.323.0";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -90,6 +90,8 @@ class Connector {
90
90
  if (cachedResp) {
91
91
  // Report that we've seen a race condition on updating session state
92
92
  (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ['Session/RaceCondition/Seen'], undefined, _features.FEATURE_NAMES.metrics, this.#agentRef.ee);
93
+ // The other tab/agent that wrote this cached response also wrote its own serverTimeDiff to the session around the same time. TimeKeeper was constructed before that happened, so re-check the session now to pick it up.
94
+ this.#agentRef.runtime.timeKeeper.processStoredDiff();
93
95
  // `cachedRumResponse` is stored flat (`{ app, ...flags }`, matching the old v1 RUM response), so re-nest it before applying.
94
96
  const {
95
97
  app,
@@ -121,7 +121,9 @@ class Aggregate extends _aggregateBase.AggregateBase {
121
121
  if (ctx.dt) {
122
122
  event.spanId = ctx.dt.spanId;
123
123
  event.traceId = ctx.dt.traceId;
124
- event.spanTimestamp = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp));
124
+ // ctx.dt.timestampCorrected holds the already NR-server-time-corrected value (see distributed-tracing.js) if
125
+ // timeKeeper was ready when the request started -- reuse it if present, otherwise correct it here now, to avoid double-correcting.
126
+ event.spanTimestamp = ctx.dt.timestampCorrected ?? Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp));
125
127
  }
126
128
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', (0, _stringify.stringify)(event.gql).length);
127
129
 
@@ -217,6 +219,10 @@ class Aggregate extends _aggregateBase.AggregateBase {
217
219
  addKey: addStringRaw,
218
220
  addVal: addStringWithTruncation
219
221
  });
222
+
223
+ // .length is UTF-16 code units, not true UTF-8 byte size, so this undercounts multi-byte characters -- but it reuses the
224
+ // already-serialized payloadAttrs strings instead of re-stringifying/byte-counting, which is worth the imprecision here.
225
+ if (payloadAttrs.length) this.reportSupportabilityMetric('Ajax/Events/Payload/Bytes-Added', payloadAttrs.join(';').length);
220
226
  const attrParts = [...regularAttrs, ...payloadAttrs];
221
227
  fields.unshift((0, _belSerializer.numeric)(attrParts.length));
222
228
  insert += fields.join(',');
@@ -9,7 +9,7 @@ var _parseUrl = require("../../../common/url/parse-url");
9
9
  var _runtime = require("../../../common/constants/runtime");
10
10
  var _stringify = require("../../../common/util/stringify");
11
11
  /**
12
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
12
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
13
13
  * SPDX-License-Identifier: Apache-2.0
14
14
  */
15
15
 
@@ -31,17 +31,25 @@ class DT {
31
31
  var spanId = (0, _uniqueId.generateSpanId)();
32
32
  var traceId = (0, _uniqueId.generateTraceId)();
33
33
  var timestamp = Date.now();
34
+ // Correct to NR server time when timeKeeper is available/ready; if it isn't connected yet (e.g. RUM hasn't
35
+ // completed), accept the uncorrected local timestamp. This only affects the receiving APM agent's computed
36
+ // transportDuration value on its Span -- it is not used for anything else in this payload.
37
+ var timestampCorrected;
38
+ if (this.agentRef.runtime?.timeKeeper?.ready) {
39
+ timestampCorrected = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(timestamp));
40
+ }
34
41
  var payload = {
35
42
  spanId,
36
43
  traceId,
37
- timestamp
44
+ timestamp,
45
+ timestampCorrected
38
46
  };
39
47
  if (parsedOrigin.sameOrigin || this.isAllowedOrigin(parsedOrigin) && this.useTraceContextHeadersForCors()) {
40
48
  payload.traceContextParentHeader = this.generateTraceContextParentHeader(spanId, traceId);
41
- payload.traceContextStateHeader = this.generateTraceContextStateHeader(spanId, timestamp, accountId, agentId, trustKey);
49
+ payload.traceContextStateHeader = this.generateTraceContextStateHeader(spanId, timestampCorrected ?? timestamp, accountId, agentId, trustKey);
42
50
  }
43
51
  if (parsedOrigin.sameOrigin && !this.excludeNewrelicHeader() || !parsedOrigin.sameOrigin && this.isAllowedOrigin(parsedOrigin) && this.useNewrelicHeaderForCors()) {
44
- payload.newrelicHeader = this.generateTraceHeader(spanId, traceId, timestamp, accountId, agentId, trustKey);
52
+ payload.newrelicHeader = this.generateTraceHeader(spanId, traceId, timestampCorrected ?? timestamp, accountId, agentId, trustKey);
45
53
  }
46
54
  return payload;
47
55
  }
@@ -146,13 +146,18 @@ class Aggregate extends _aggregateBase.AggregateBase {
146
146
  }
147
147
  switchToFull() {
148
148
  if (!this.entitled || this.blocked) return;
149
+ const prevMode = this.mode;
149
150
  this.mode = _constants2.MODE.FULL;
151
+ this.#writeToStorage({
152
+ sessionReplayMode: this.mode
153
+ });
154
+
150
155
  // if the error was noticed AFTER the recorder was already imported....
151
156
  if (this.recorder && this.initialized) {
157
+ if (prevMode === _constants2.MODE.ERROR) {
158
+ this.agentRef.runtime.harvester.triggerHarvestFor(this);
159
+ }
152
160
  if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(_constants.TRIGGERS.SWITCH_TO_FULL, this.mode); // off --> full
153
- this.#writeToStorage({
154
- sessionReplayMode: this.mode
155
- });
156
161
  } else {
157
162
  this.initializeRecording(_constants2.MODE.FULL, true, _constants.TRIGGERS.SWITCH_TO_FULL);
158
163
  }
@@ -90,12 +90,12 @@ class Aggregate extends _aggregateBase.AggregateBase {
90
90
  (0, _registerHandler.registerHandler)('bstApi', (...args) => this.traceStorage.storeNode(...args), this.featureName, this.ee);
91
91
  (0, _registerHandler.registerHandler)('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee);
92
92
  (0, _registerHandler.registerHandler)('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee);
93
- if (this.mode === _constants2.MODE.ERROR) {
94
- /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
95
- (0, _registerHandler.registerHandler)('trace-jserror', () => {
96
- if (this.mode === _constants2.MODE.ERROR) this.switchToFull();
97
- }, this.featureName, this.ee);
98
- }
93
+ }
94
+ if (this.mode === _constants2.MODE.ERROR) {
95
+ /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
96
+ (0, _registerHandler.registerHandler)('trace-jserror', () => {
97
+ if (this.mode === _constants2.MODE.ERROR) this.switchToFull();
98
+ }, this.featureName, this.ee);
99
99
  }
100
100
  this.agentRef.runtime.session.write({
101
101
  sessionTraceMode: this.mode
@@ -6,8 +6,11 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.AjaxNode = void 0;
7
7
  var _belSerializer = require("../../../common/serialize/bel-serializer");
8
8
  var _payloads = require("../../../common/payloads/payloads");
9
- var _constants = require("../../ajax/constants");
10
- var _constants2 = require("../constants");
9
+ var _handle = require("../../../common/event-emitter/handle");
10
+ var _features = require("../../../loaders/features/features");
11
+ var _constants = require("../../metrics/constants");
12
+ var _constants2 = require("../../ajax/constants");
13
+ var _constants3 = require("../constants");
11
14
  var _belNode = require("./bel-node");
12
15
  /**
13
16
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
@@ -17,7 +20,7 @@ var _belNode = require("./bel-node");
17
20
  class AjaxNode extends _belNode.BelNode {
18
21
  constructor(ajaxEvent, ajaxContext) {
19
22
  super();
20
- this.belType = _constants2.NODE_TYPE.AJAX;
23
+ this.belType = _constants3.NODE_TYPE.AJAX;
21
24
  this.method = ajaxEvent.method;
22
25
  this.status = ajaxEvent.status;
23
26
  this.domain = ajaxEvent.domain;
@@ -30,7 +33,7 @@ class AjaxNode extends _belNode.BelNode {
30
33
  this.spanTimestamp = ajaxEvent.spanTimestamp;
31
34
  this.gql = ajaxEvent.gql;
32
35
  this.targetAttributes = ajaxEvent.targetAttributes;
33
- this[_constants.AJAX_ID] = ajaxEvent[_constants.AJAX_ID]; // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
36
+ this[_constants2.AJAX_ID] = ajaxEvent[_constants2.AJAX_ID]; // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
34
37
 
35
38
  // optional payload metadata attributes
36
39
  this.requestBody = ajaxEvent.requestBody;
@@ -74,7 +77,7 @@ class AjaxNode extends _belNode.BelNode {
74
77
  addVal: addString
75
78
  });
76
79
  regularAttrs.push(...(0, _belSerializer.addCustomAttributes)({
77
- [_constants.AJAX_ID]: this[_constants.AJAX_ID]
80
+ [_constants2.AJAX_ID]: this[_constants2.AJAX_ID]
78
81
  }, {
79
82
  addKey: addStringRaw,
80
83
  addVal: addStringRaw
@@ -101,6 +104,10 @@ class AjaxNode extends _belNode.BelNode {
101
104
  addKey: addStringRaw,
102
105
  addVal: addStringWithTruncation
103
106
  });
107
+
108
+ // .length is UTF-16 code units, not true UTF-8 byte size, so this undercounts multi-byte characters -- but it reuses the
109
+ // already-serialized payloadAttrs strings instead of re-stringifying/byte-counting, which is worth the imprecision here.
110
+ if (payloadAttrs.length) (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Payload/Bytes-Added', payloadAttrs.join(';').length], undefined, _features.FEATURE_NAMES.metrics, agentRef.ee);
104
111
  let allAttachedNodes = [...regularAttrs, ...payloadAttrs];
105
112
  this.children.forEach(node => allAttachedNodes.push(node.serialize())); // no children is expected under ajax nodes at this time
106
113
 
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.322.0";
14
+ export const VERSION = "1.323.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.322.0";
14
+ export const VERSION = "1.323.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -84,6 +84,8 @@ export class Connector {
84
84
  if (cachedResp) {
85
85
  // Report that we've seen a race condition on updating session state
86
86
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Session/RaceCondition/Seen'], undefined, FEATURE_NAMES.metrics, this.#agentRef.ee);
87
+ // The other tab/agent that wrote this cached response also wrote its own serverTimeDiff to the session around the same time. TimeKeeper was constructed before that happened, so re-check the session now to pick it up.
88
+ this.#agentRef.runtime.timeKeeper.processStoredDiff();
87
89
  // `cachedRumResponse` is stored flat (`{ app, ...flags }`, matching the old v1 RUM response), so re-nest it before applying.
88
90
  const {
89
91
  app,
@@ -114,7 +114,9 @@ export class Aggregate extends AggregateBase {
114
114
  if (ctx.dt) {
115
115
  event.spanId = ctx.dt.spanId;
116
116
  event.traceId = ctx.dt.traceId;
117
- event.spanTimestamp = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp));
117
+ // ctx.dt.timestampCorrected holds the already NR-server-time-corrected value (see distributed-tracing.js) if
118
+ // timeKeeper was ready when the request started -- reuse it if present, otherwise correct it here now, to avoid double-correcting.
119
+ event.spanTimestamp = ctx.dt.timestampCorrected ?? Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp));
118
120
  }
119
121
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length);
120
122
 
@@ -210,6 +212,10 @@ export class Aggregate extends AggregateBase {
210
212
  addKey: addStringRaw,
211
213
  addVal: addStringWithTruncation
212
214
  });
215
+
216
+ // .length is UTF-16 code units, not true UTF-8 byte size, so this undercounts multi-byte characters -- but it reuses the
217
+ // already-serialized payloadAttrs strings instead of re-stringifying/byte-counting, which is worth the imprecision here.
218
+ if (payloadAttrs.length) this.reportSupportabilityMetric('Ajax/Events/Payload/Bytes-Added', payloadAttrs.join(';').length);
213
219
  const attrParts = [...regularAttrs, ...payloadAttrs];
214
220
  fields.unshift(numeric(attrParts.length));
215
221
  insert += fields.join(',');
@@ -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 { generateSpanId, generateTraceId } from '../../../common/ids/unique-id';
@@ -24,17 +24,25 @@ export class DT {
24
24
  var spanId = generateSpanId();
25
25
  var traceId = generateTraceId();
26
26
  var timestamp = Date.now();
27
+ // Correct to NR server time when timeKeeper is available/ready; if it isn't connected yet (e.g. RUM hasn't
28
+ // completed), accept the uncorrected local timestamp. This only affects the receiving APM agent's computed
29
+ // transportDuration value on its Span -- it is not used for anything else in this payload.
30
+ var timestampCorrected;
31
+ if (this.agentRef.runtime?.timeKeeper?.ready) {
32
+ timestampCorrected = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(timestamp));
33
+ }
27
34
  var payload = {
28
35
  spanId,
29
36
  traceId,
30
- timestamp
37
+ timestamp,
38
+ timestampCorrected
31
39
  };
32
40
  if (parsedOrigin.sameOrigin || this.isAllowedOrigin(parsedOrigin) && this.useTraceContextHeadersForCors()) {
33
41
  payload.traceContextParentHeader = this.generateTraceContextParentHeader(spanId, traceId);
34
- payload.traceContextStateHeader = this.generateTraceContextStateHeader(spanId, timestamp, accountId, agentId, trustKey);
42
+ payload.traceContextStateHeader = this.generateTraceContextStateHeader(spanId, timestampCorrected ?? timestamp, accountId, agentId, trustKey);
35
43
  }
36
44
  if (parsedOrigin.sameOrigin && !this.excludeNewrelicHeader() || !parsedOrigin.sameOrigin && this.isAllowedOrigin(parsedOrigin) && this.useNewrelicHeaderForCors()) {
37
- payload.newrelicHeader = this.generateTraceHeader(spanId, traceId, timestamp, accountId, agentId, trustKey);
45
+ payload.newrelicHeader = this.generateTraceHeader(spanId, traceId, timestampCorrected ?? timestamp, accountId, agentId, trustKey);
38
46
  }
39
47
  return payload;
40
48
  }
@@ -142,13 +142,18 @@ export class Aggregate extends AggregateBase {
142
142
  }
143
143
  switchToFull() {
144
144
  if (!this.entitled || this.blocked) return;
145
+ const prevMode = this.mode;
145
146
  this.mode = MODE.FULL;
147
+ this.#writeToStorage({
148
+ sessionReplayMode: this.mode
149
+ });
150
+
146
151
  // if the error was noticed AFTER the recorder was already imported....
147
152
  if (this.recorder && this.initialized) {
153
+ if (prevMode === MODE.ERROR) {
154
+ this.agentRef.runtime.harvester.triggerHarvestFor(this);
155
+ }
148
156
  if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(TRIGGERS.SWITCH_TO_FULL, this.mode); // off --> full
149
- this.#writeToStorage({
150
- sessionReplayMode: this.mode
151
- });
152
157
  } else {
153
158
  this.initializeRecording(MODE.FULL, true, TRIGGERS.SWITCH_TO_FULL);
154
159
  }
@@ -84,12 +84,12 @@ export class Aggregate extends AggregateBase {
84
84
  registerHandler('bstApi', (...args) => this.traceStorage.storeNode(...args), this.featureName, this.ee);
85
85
  registerHandler('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee);
86
86
  registerHandler('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee);
87
- if (this.mode === MODE.ERROR) {
88
- /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
89
- registerHandler('trace-jserror', () => {
90
- if (this.mode === MODE.ERROR) this.switchToFull();
91
- }, this.featureName, this.ee);
92
- }
87
+ }
88
+ if (this.mode === MODE.ERROR) {
89
+ /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
90
+ registerHandler('trace-jserror', () => {
91
+ if (this.mode === MODE.ERROR) this.switchToFull();
92
+ }, this.featureName, this.ee);
93
93
  }
94
94
  this.agentRef.runtime.session.write({
95
95
  sessionTraceMode: this.mode
@@ -4,6 +4,9 @@
4
4
  */
5
5
  import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer';
6
6
  import { createStringAdders } from '../../../common/payloads/payloads';
7
+ import { handle } from '../../../common/event-emitter/handle';
8
+ import { FEATURE_NAMES } from '../../../loaders/features/features';
9
+ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
7
10
  import { AJAX_ID } from '../../ajax/constants';
8
11
  import { NODE_TYPE } from '../constants';
9
12
  import { BelNode } from './bel-node';
@@ -94,6 +97,10 @@ export class AjaxNode extends BelNode {
94
97
  addKey: addStringRaw,
95
98
  addVal: addStringWithTruncation
96
99
  });
100
+
101
+ // .length is UTF-16 code units, not true UTF-8 byte size, so this undercounts multi-byte characters -- but it reuses the
102
+ // already-serialized payloadAttrs strings instead of re-stringifying/byte-counting, which is worth the imprecision here.
103
+ if (payloadAttrs.length) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Payload/Bytes-Added', payloadAttrs.join(';').length], undefined, FEATURE_NAMES.metrics, agentRef.ee);
97
104
  let allAttachedNodes = [...regularAttrs, ...payloadAttrs];
98
105
  this.children.forEach(node => allAttachedNodes.push(node.serialize())); // no children is expected under ajax nodes at this time
99
106
 
@@ -1 +1 @@
1
- {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../../src/common/harvest/connector.js"],"names":[],"mappings":"AAmBA;IAKE,2BAeC;IAED,+CAiBC;;CA+GF"}
1
+ {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../../../src/common/harvest/connector.js"],"names":[],"mappings":"AAmBA;IAKE,2BAeC;IAED,+CAiBC;;CAiHF"}
@@ -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,iDAuEC;CACF;8BA1N6B,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,0GA2FC;IAED,+CAOC;IAED,iDA2EC;CACF;8BA9N6B,4BAA4B;2BAK/B,gCAAgC"}
@@ -5,6 +5,7 @@ export class DT {
5
5
  spanId: string;
6
6
  traceId: string;
7
7
  timestamp: number;
8
+ timestampCorrected: number | undefined;
8
9
  } | null;
9
10
  generateTraceContextParentHeader(spanId: any, traceId: any): string;
10
11
  generateTraceContextStateHeader(spanId: any, timestamp: any, accountId: any, appId: any, trustKey: any): string;
@@ -1 +1 @@
1
- {"version":3,"file":"distributed-tracing.d.ts","sourceRoot":"","sources":["../../../../../src/features/ajax/instrument/distributed-tracing.js"],"names":[],"mappings":"AASA;IACE,2BAEC;IADC,cAAwB;IAG1B;;;;aAsCC;IAED,oEAEC;IAED,gHASC;IAED,yHAsBC;IAID,4CAEC;IAED,4CAkBC;IAGD,iCAMC;IAED,oCAMC;IAED,yCAMC;CACF"}
1
+ {"version":3,"file":"distributed-tracing.d.ts","sourceRoot":"","sources":["../../../../../src/features/ajax/instrument/distributed-tracing.js"],"names":[],"mappings":"AASA;IACE,2BAEC;IADC,cAAwB;IAG1B;;;;;aA8CC;IAED,oEAEC;IAED,gHASC;IAED,yHAsBC;IAID,4CAEC;IAED,4CAkBC;IAGD,iCAMC;IAED,oCAMC;IAED,yCAMC;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AA2BA;IACE,2BAAiC;IAIjC,sCA+FC;IAlGD,WAAW;IAOT,uBAA0D;IAE1D,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;IAqEvG,0BAEC;IAED,0BAMC;IAED,qBAUC;IAED;;;;;;OAMG;IACH,4BALW,OAAO,iBACP,OAAO;;;;;;QAEL,IAAI,CA+ChB;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;;CAOF;8BAjY6B,4BAA4B;2BAe/B,gCAAgC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AA2BA;IACE,2BAAiC;IAIjC,sCA+FC;IAlGD,WAAW;IAOT,uBAA0D;IAE1D,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;IAqEvG,0BAEC;IAED,0BAMC;IAED,qBAeC;IAED;;;;;;OAMG;IACH,4BALW,OAAO,iBACP,OAAO;;;;;;QAEL,IAAI,CA+ChB;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;;CAOF;8BAtY6B,4BAA4B;2BAe/B,gCAAgC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ajax-node.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/ajax-node.js"],"names":[],"mappings":"AAUA;IACE,8CA8BC;IA5BC,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;IAIlD,iBAAwC;IACxC,oBAA8C;IAC9C,kBAA0C;IAC1C,kBAA0C;IAC1C,qBAAgD;IAK9C,iBAAoE;IAKxE,iFAkDC;IAnEC,sBAAkC;CAoErC;wBAtFuB,YAAY"}
1
+ {"version":3,"file":"ajax-node.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/ajax-node.js"],"names":[],"mappings":"AAaA;IACE,8CA8BC;IA5BC,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;IAIlD,iBAAwC;IACxC,oBAA8C;IAC9C,kBAA0C;IAC1C,kBAA0C;IAC1C,qBAAgD;IAK9C,iBAAoE;IAKxE,iFAsDC;IAvEC,sBAAkC;CAwErC;wBA1FuB,YAAY"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.322.0",
3
+ "version": "1.323.0",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -206,7 +206,10 @@
206
206
  "test-server": "node ./tools/wdio/bin/server",
207
207
  "lt:update-browsers": "node ./tools/browsers-lists/lt-update-supported.mjs",
208
208
  "lt:upload-webview-assets": "node ./tools/lambda-test/upload-webview-assets.mjs",
209
+ "build:frameworks": "npm run build:npm && npm --prefix ./tools/test-builds run build-frameworks",
209
210
  "tools:test-builds": "npm --prefix ./tools/test-builds run build-all",
211
+ "test-builds:bump-latest": "node ./tools/test-builds/bump-latest.mjs",
212
+ "test-builds:framework-report": "node ./tools/test-builds/generate-framework-report.mjs",
210
213
  "third-party-updates": "oss third-party manifest --includeOptDeps && oss third-party notices --includeOptDeps",
211
214
  "prepare": "husky install",
212
215
  "npm:build:esm": "npx babel --env-name npm-esm --out-dir dist/esm --out-file-extension .js ./src",
@@ -73,6 +73,8 @@ export class Connector {
73
73
  if (cachedResp) {
74
74
  // Report that we've seen a race condition on updating session state
75
75
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Session/RaceCondition/Seen'], undefined, FEATURE_NAMES.metrics, this.#agentRef.ee)
76
+ // The other tab/agent that wrote this cached response also wrote its own serverTimeDiff to the session around the same time. TimeKeeper was constructed before that happened, so re-check the session now to pick it up.
77
+ this.#agentRef.runtime.timeKeeper.processStoredDiff()
76
78
  // `cachedRumResponse` is stored flat (`{ app, ...flags }`, matching the old v1 RUM response), so re-nest it before applying.
77
79
  const { app, ...config } = cachedResp
78
80
  this.#applyConnectResponse({ app, config })
@@ -129,9 +129,9 @@ export class Aggregate extends AggregateBase {
129
129
  if (ctx.dt) {
130
130
  event.spanId = ctx.dt.spanId
131
131
  event.traceId = ctx.dt.traceId
132
- event.spanTimestamp = Math.floor(
133
- this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp)
134
- )
132
+ // ctx.dt.timestampCorrected holds the already NR-server-time-corrected value (see distributed-tracing.js) if
133
+ // timeKeeper was ready when the request started -- reuse it if present, otherwise correct it here now, to avoid double-correcting.
134
+ event.spanTimestamp = ctx.dt.timestampCorrected ?? Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp))
135
135
  }
136
136
 
137
137
  if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length)
@@ -212,6 +212,10 @@ export class Aggregate extends AggregateBase {
212
212
  ...(event.responseHeaders ? { responseHeaders: event.responseHeaders } : {})
213
213
  }, { addKey: addStringRaw, addVal: addStringWithTruncation })
214
214
 
215
+ // .length is UTF-16 code units, not true UTF-8 byte size, so this undercounts multi-byte characters -- but it reuses the
216
+ // already-serialized payloadAttrs strings instead of re-stringifying/byte-counting, which is worth the imprecision here.
217
+ if (payloadAttrs.length) this.reportSupportabilityMetric('Ajax/Events/Payload/Bytes-Added', payloadAttrs.join(';').length)
218
+
215
219
  const attrParts = [...regularAttrs, ...payloadAttrs]
216
220
  fields.unshift(numeric(attrParts.length))
217
221
 
@@ -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 { generateSpanId, generateTraceId } from '../../../common/ids/unique-id'
@@ -29,23 +29,31 @@ export class DT {
29
29
  var spanId = generateSpanId()
30
30
  var traceId = generateTraceId()
31
31
  var timestamp = Date.now()
32
+ // Correct to NR server time when timeKeeper is available/ready; if it isn't connected yet (e.g. RUM hasn't
33
+ // completed), accept the uncorrected local timestamp. This only affects the receiving APM agent's computed
34
+ // transportDuration value on its Span -- it is not used for anything else in this payload.
35
+ var timestampCorrected
36
+ if (this.agentRef.runtime?.timeKeeper?.ready) {
37
+ timestampCorrected = Math.floor(this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(timestamp))
38
+ }
32
39
 
33
40
  var payload = {
34
41
  spanId,
35
42
  traceId,
36
- timestamp
43
+ timestamp,
44
+ timestampCorrected
37
45
  }
38
46
 
39
47
  if (parsedOrigin.sameOrigin ||
40
48
  (this.isAllowedOrigin(parsedOrigin) && this.useTraceContextHeadersForCors())) {
41
49
  payload.traceContextParentHeader = this.generateTraceContextParentHeader(spanId, traceId)
42
- payload.traceContextStateHeader = this.generateTraceContextStateHeader(spanId, timestamp,
50
+ payload.traceContextStateHeader = this.generateTraceContextStateHeader(spanId, timestampCorrected ?? timestamp,
43
51
  accountId, agentId, trustKey)
44
52
  }
45
53
 
46
54
  if ((parsedOrigin.sameOrigin && !this.excludeNewrelicHeader()) ||
47
55
  (!parsedOrigin.sameOrigin && this.isAllowedOrigin(parsedOrigin) && this.useNewrelicHeaderForCors())) {
48
- payload.newrelicHeader = this.generateTraceHeader(spanId, traceId, timestamp, accountId,
56
+ payload.newrelicHeader = this.generateTraceHeader(spanId, traceId, timestampCorrected ?? timestamp, accountId,
49
57
  agentId, trustKey)
50
58
  }
51
59
 
@@ -141,11 +141,16 @@ export class Aggregate extends AggregateBase {
141
141
 
142
142
  switchToFull () {
143
143
  if (!this.entitled || this.blocked) return
144
+ const prevMode = this.mode
144
145
  this.mode = MODE.FULL
146
+ this.#writeToStorage({ sessionReplayMode: this.mode })
147
+
145
148
  // if the error was noticed AFTER the recorder was already imported....
146
149
  if (this.recorder && this.initialized) {
150
+ if (prevMode === MODE.ERROR) {
151
+ this.agentRef.runtime.harvester.triggerHarvestFor(this)
152
+ }
147
153
  if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(TRIGGERS.SWITCH_TO_FULL, this.mode) // off --> full
148
- this.#writeToStorage({ sessionReplayMode: this.mode })
149
154
  } else {
150
155
  this.initializeRecording(MODE.FULL, true, TRIGGERS.SWITCH_TO_FULL)
151
156
  }
@@ -90,13 +90,13 @@ export class Aggregate extends AggregateBase {
90
90
  registerHandler('bstApi', (...args) => this.traceStorage.storeNode(...args), this.featureName, this.ee)
91
91
  registerHandler('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee)
92
92
  registerHandler('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee)
93
+ }
93
94
 
94
- if (this.mode === MODE.ERROR) {
95
- /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
96
- registerHandler('trace-jserror', () => {
97
- if (this.mode === MODE.ERROR) this.switchToFull()
98
- }, this.featureName, this.ee)
99
- }
95
+ if (this.mode === MODE.ERROR) {
96
+ /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
97
+ registerHandler('trace-jserror', () => {
98
+ if (this.mode === MODE.ERROR) this.switchToFull()
99
+ }, this.featureName, this.ee)
100
100
  }
101
101
  this.agentRef.runtime.session.write({ sessionTraceMode: this.mode })
102
102
 
@@ -4,6 +4,9 @@
4
4
  */
5
5
  import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer'
6
6
  import { createStringAdders } from '../../../common/payloads/payloads'
7
+ import { handle } from '../../../common/event-emitter/handle'
8
+ import { FEATURE_NAMES } from '../../../loaders/features/features'
9
+ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
7
10
  import { AJAX_ID } from '../../ajax/constants'
8
11
  import { NODE_TYPE } from '../constants'
9
12
  import { BelNode } from './bel-node'
@@ -83,6 +86,10 @@ export class AjaxNode extends BelNode {
83
86
  ...(this.responseHeaders ? { responseHeaders: this.responseHeaders } : {})
84
87
  }, { addKey: addStringRaw, addVal: addStringWithTruncation })
85
88
 
89
+ // .length is UTF-16 code units, not true UTF-8 byte size, so this undercounts multi-byte characters -- but it reuses the
90
+ // already-serialized payloadAttrs strings instead of re-stringifying/byte-counting, which is worth the imprecision here.
91
+ if (payloadAttrs.length) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Payload/Bytes-Added', payloadAttrs.join(';').length], undefined, FEATURE_NAMES.metrics, agentRef.ee)
92
+
86
93
  let allAttachedNodes = [...regularAttrs, ...payloadAttrs]
87
94
  this.children.forEach(node => allAttachedNodes.push(node.serialize())) // no children is expected under ajax nodes at this time
88
95