@newrelic/browser-agent 1.320.0 → 1.320.1-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 (121) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cjs/common/config/runtime.js +1 -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/event-listener/event-listener-opts.js +1 -1
  6. package/dist/cjs/common/harvest/connector.js +200 -0
  7. package/dist/cjs/common/harvest/harvester.js +5 -8
  8. package/dist/cjs/common/harvest/send.js +8 -7
  9. package/dist/cjs/common/payloads/payloads.js +3 -2
  10. package/dist/cjs/common/serialize/bel-serializer.js +17 -4
  11. package/dist/cjs/common/timer/interaction-timer.js +2 -2
  12. package/dist/cjs/common/unload/eol.js +3 -3
  13. package/dist/cjs/common/util/console.js +2 -1
  14. package/dist/cjs/common/util/feature-flags.js +2 -1
  15. package/dist/cjs/common/v2/mfe-vitals.js +34 -11
  16. package/dist/cjs/common/window/page-visibility.js +1 -1
  17. package/dist/cjs/features/ajax/aggregate/index.js +19 -8
  18. package/dist/cjs/features/page_view_event/aggregate/index.js +2 -6
  19. package/dist/cjs/features/page_view_event/aggregate_v2/index.js +109 -0
  20. package/dist/cjs/features/page_view_event/instrument/index.js +6 -1
  21. package/dist/cjs/features/page_view_timing/aggregate/index.js +33 -14
  22. package/dist/cjs/features/session_replay/instrument/index.js +9 -0
  23. package/dist/cjs/features/session_replay/shared/recorder.js +2 -1
  24. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +16 -4
  25. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +10 -3
  26. package/dist/cjs/features/utils/aggregate-base.js +0 -12
  27. package/dist/cjs/features/utils/instrument-base.js +72 -29
  28. package/dist/cjs/loaders/agent.js +2 -1
  29. package/dist/cjs/loaders/api/consent.js +2 -2
  30. package/dist/cjs/loaders/features/features.js +2 -1
  31. package/dist/esm/common/config/runtime.js +1 -0
  32. package/dist/esm/common/constants/env.cdn.js +1 -1
  33. package/dist/esm/common/constants/env.npm.js +1 -1
  34. package/dist/esm/common/event-listener/event-listener-opts.js +1 -1
  35. package/dist/esm/common/harvest/connector.js +193 -0
  36. package/dist/esm/common/harvest/harvester.js +5 -8
  37. package/dist/esm/common/harvest/send.js +10 -9
  38. package/dist/esm/common/payloads/payloads.js +3 -2
  39. package/dist/esm/common/serialize/bel-serializer.js +17 -4
  40. package/dist/esm/common/timer/interaction-timer.js +2 -2
  41. package/dist/esm/common/unload/eol.js +3 -3
  42. package/dist/esm/common/util/console.js +2 -1
  43. package/dist/esm/common/util/feature-flags.js +2 -1
  44. package/dist/esm/common/v2/mfe-vitals.js +34 -11
  45. package/dist/esm/common/window/page-visibility.js +1 -1
  46. package/dist/esm/features/ajax/aggregate/index.js +19 -8
  47. package/dist/esm/features/page_view_event/aggregate/index.js +2 -6
  48. package/dist/esm/features/page_view_event/aggregate_v2/index.js +100 -0
  49. package/dist/esm/features/page_view_event/instrument/index.js +6 -1
  50. package/dist/esm/features/page_view_timing/aggregate/index.js +33 -14
  51. package/dist/esm/features/session_replay/instrument/index.js +9 -0
  52. package/dist/esm/features/session_replay/shared/recorder.js +2 -1
  53. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +16 -4
  54. package/dist/esm/features/soft_navigations/aggregate/interaction.js +10 -3
  55. package/dist/esm/features/utils/aggregate-base.js +0 -12
  56. package/dist/esm/features/utils/instrument-base.js +71 -28
  57. package/dist/esm/loaders/agent.js +2 -1
  58. package/dist/esm/loaders/api/consent.js +2 -2
  59. package/dist/esm/loaders/features/features.js +1 -0
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/dist/types/common/config/runtime.d.ts.map +1 -1
  62. package/dist/types/common/event-listener/event-listener-opts.d.ts +1 -1
  63. package/dist/types/common/harvest/connector.d.ts +6 -0
  64. package/dist/types/common/harvest/connector.d.ts.map +1 -0
  65. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  66. package/dist/types/common/harvest/send.d.ts.map +1 -1
  67. package/dist/types/common/payloads/payloads.d.ts +3 -2
  68. package/dist/types/common/payloads/payloads.d.ts.map +1 -1
  69. package/dist/types/common/serialize/bel-serializer.d.ts +9 -1
  70. package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
  71. package/dist/types/common/unload/eol.d.ts +1 -1
  72. package/dist/types/common/unload/eol.d.ts.map +1 -1
  73. package/dist/types/common/util/console.d.ts +2 -1
  74. package/dist/types/common/util/console.d.ts.map +1 -1
  75. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
  76. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  77. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  78. package/dist/types/features/page_view_event/aggregate_v2/index.d.ts +20 -0
  79. package/dist/types/features/page_view_event/aggregate_v2/index.d.ts.map +1 -0
  80. package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
  81. package/dist/types/features/page_view_timing/aggregate/index.d.ts +0 -1
  82. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  83. package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
  84. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  85. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  86. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  87. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  88. package/dist/types/features/utils/aggregate-base.d.ts +0 -5
  89. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  90. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  91. package/dist/types/loaders/agent.d.ts.map +1 -1
  92. package/dist/types/loaders/features/features.d.ts +1 -0
  93. package/dist/types/loaders/features/features.d.ts.map +1 -1
  94. package/package.json +2 -2
  95. package/src/common/config/runtime.js +1 -0
  96. package/src/common/event-listener/event-listener-opts.js +1 -1
  97. package/src/common/harvest/connector.js +168 -0
  98. package/src/common/harvest/harvester.js +7 -9
  99. package/src/common/harvest/send.js +9 -7
  100. package/src/common/payloads/payloads.js +3 -2
  101. package/src/common/serialize/bel-serializer.js +13 -4
  102. package/src/common/timer/interaction-timer.js +2 -2
  103. package/src/common/unload/eol.js +3 -3
  104. package/src/common/util/console.js +2 -1
  105. package/src/common/util/feature-flags.js +1 -1
  106. package/src/common/v2/mfe-vitals.js +39 -12
  107. package/src/common/window/page-visibility.js +1 -1
  108. package/src/features/ajax/aggregate/index.js +12 -9
  109. package/src/features/page_view_event/aggregate/index.js +2 -6
  110. package/src/features/page_view_event/aggregate_v2/index.js +97 -0
  111. package/src/features/page_view_event/instrument/index.js +6 -1
  112. package/src/features/page_view_timing/aggregate/index.js +24 -15
  113. package/src/features/session_replay/instrument/index.js +9 -0
  114. package/src/features/session_replay/shared/recorder.js +2 -1
  115. package/src/features/soft_navigations/aggregate/ajax-node.js +10 -7
  116. package/src/features/soft_navigations/aggregate/interaction.js +6 -5
  117. package/src/features/utils/aggregate-base.js +0 -12
  118. package/src/features/utils/instrument-base.js +71 -25
  119. package/src/loaders/agent.js +2 -1
  120. package/src/loaders/api/consent.js +2 -2
  121. package/src/loaders/features/features.js +1 -0
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.320.0";
14
+ export const VERSION = "1.320.1-rc.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.320.0";
14
+ export const VERSION = "1.320.1-rc.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2020-2025 New Relic, Inc. All rights reserved.
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  export function eventListenerOpts(useCapture, abortSignal) {
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { isBrowserScope } from '../constants/runtime';
7
+ import { send } from './send';
8
+ import { TimeKeeper } from '../timing/time-keeper';
9
+ import { activateFeatures } from '../util/feature-flags';
10
+ import { now } from '../timing/now';
11
+ import { warn } from '../util/console';
12
+ import { VERSION } from "../constants/env.npm";
13
+ import { CONNECT, FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../loaders/features/features';
14
+ import { handle } from '../event-emitter/handle';
15
+ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants';
16
+ const MAX_CONNECT_ATTEMPTS = 3;
17
+ const CONNECT_RETRY_BASE_MS = 3000; // affects each retry window proportionally (attempt 2: 0-3s, 3: 0-6s, 4: 0-12s, etc)
18
+
19
+ export class Connector {
20
+ #agentRef;
21
+ #connectStartTime;
22
+ #numOfAttempts = 0;
23
+ constructor(agentRef) {
24
+ if (!agentRef.init.feature_flags.includes('rum_v2')) return; // the Connector does not run in v1; legacy PVE has its own BCS handling
25
+ this.#agentRef = agentRef;
26
+ agentRef.runtime.timeKeeper = new TimeKeeper(agentRef.runtime.session);
27
+ if (isBrowserScope) {
28
+ const cached = agentRef.runtime.session && agentRef.runtime.session.state.cachedRumResponse;
29
+ if (cached) {
30
+ // `cachedRumResponse` is stored flat (`{ app, ...flags }`, matching the old v1 RUM response), so re-nest it before applying.
31
+ const {
32
+ app,
33
+ ...config
34
+ } = cached;
35
+ this.#applyConnectResponse({
36
+ app,
37
+ config
38
+ });
39
+ return;
40
+ }
41
+ }
42
+ this.makeConnectRequest();
43
+ }
44
+ makeConnectRequest(retryHeaders) {
45
+ this.#connectStartTime = now();
46
+ return send(this.#agentRef, {
47
+ endpoint: "browser/connect/2/".concat(this.#agentRef.info.licenseKey),
48
+ payload: {
49
+ body: {},
50
+ qs: {
51
+ a: this.#agentRef.info.applicationID,
52
+ v: VERSION,
53
+ s: this.#agentRef.runtime.session?.state.value || '0'
54
+ }
55
+ },
56
+ localOpts: {
57
+ sendEmptyBody: true,
58
+ headers: retryHeaders
59
+ },
60
+ raw: true,
61
+ featureName: CONNECT,
62
+ cbFinished: this.#processConnectResponse.bind(this)
63
+ });
64
+ }
65
+ #applyConnectResponse({
66
+ app,
67
+ config
68
+ }) {
69
+ if (!Object.keys(this.#agentRef.runtime.appMetadata).length) this.#agentRef.runtime.appMetadata = app;
70
+ activateFeatures(config, this.#agentRef);
71
+ }
72
+ #processConnectResponse({
73
+ sent,
74
+ status,
75
+ retry,
76
+ xhr,
77
+ responseText
78
+ }) {
79
+ const connectEndTime = now();
80
+ const session = this.#agentRef.runtime.session;
81
+ if (session) {
82
+ // Double check that there isn't a response saved (e.g. from another agent/browser tab) btwn the async time of the request and now in a race-condition.
83
+ const cachedResp = session.state.cachedRumResponse;
84
+ if (cachedResp) {
85
+ // `cachedRumResponse` is stored flat (`{ app, ...flags }`, matching the old v1 RUM response), so re-nest it before applying.
86
+ const {
87
+ app,
88
+ ...config
89
+ } = cachedResp;
90
+ this.#applyConnectResponse({
91
+ app,
92
+ config
93
+ });
94
+ return;
95
+ }
96
+ }
97
+ const shouldRetry = sent && retry; // important to note: no retry if HTTP status is 0
98
+ if (shouldRetry && ++this.#numOfAttempts < MAX_CONNECT_ATTEMPTS) {
99
+ // We use an exponential backoff calculation for the retry to avoid thundering herd, skipping any further processing.
100
+ const delay = Math.floor(Math.random() * CONNECT_RETRY_BASE_MS * 2 ** (this.#numOfAttempts - 1));
101
+ const retryHeaders = [{
102
+ key: 'X-Retry-Count',
103
+ value: this.#numOfAttempts
104
+ }, {
105
+ key: 'X-Previous-Status',
106
+ value: status
107
+ }];
108
+ setTimeout(() => this.makeConnectRequest(retryHeaders), delay);
109
+ return;
110
+ }
111
+ if (status >= 400 || status === 0) {
112
+ warn(18, status);
113
+
114
+ // Get estimated payload size of our backlog.
115
+ const textEncoder = new TextEncoder();
116
+ const payloadSize = Object.values(newrelic.ee.backlog).reduce((acc, value) => {
117
+ if (!value) return acc;
118
+ const encoded = textEncoder.encode(value);
119
+ return acc + encoded.byteLength;
120
+ }, 0);
121
+
122
+ // Send SMs about the (last) failed connect request, then abort agent.
123
+ const BCSError = 'BCS/Error/';
124
+ const sm = [{
125
+ params: {
126
+ name: BCSError + status
127
+ },
128
+ stats: {
129
+ c: 1
130
+ }
131
+ }, {
132
+ params: {
133
+ name: BCSError + 'Duration/Ms'
134
+ },
135
+ stats: {
136
+ c: 1,
137
+ t: connectEndTime - this.#connectStartTime
138
+ }
139
+ }, {
140
+ params: {
141
+ name: BCSError + 'Dropped/Bytes'
142
+ },
143
+ stats: {
144
+ c: 1,
145
+ t: payloadSize
146
+ }
147
+ }];
148
+ send(this.#agentRef, {
149
+ // note we have to send it this way because the Metrics feature will never be harvested under this branch of code (BCS failure)
150
+ endpoint: FEATURE_TO_ENDPOINT[FEATURE_NAMES.metrics],
151
+ payload: {
152
+ body: {
153
+ sm
154
+ }
155
+ },
156
+ featureName: FEATURE_NAMES.metrics
157
+ });
158
+ this.#agentRef.ee.abort();
159
+ return;
160
+ }
161
+ let resp;
162
+ try {
163
+ resp = JSON.parse(responseText);
164
+ } catch (error) {
165
+ warn(53, error);
166
+ this.#agentRef.ee.abort();
167
+ return;
168
+ }
169
+ // store the response for other pages later in same session or in parallel tabs; flatten `.config` into the old v1 RUM response shape
170
+ if (session) session.write({
171
+ cachedRumResponse: {
172
+ app: resp.app,
173
+ ...resp.config
174
+ }
175
+ });
176
+ try {
177
+ const wasReady = this.#agentRef.runtime.timeKeeper.ready;
178
+ this.#agentRef.runtime.timeKeeper.processRumRequest(xhr, this.#connectStartTime, connectEndTime, resp.app.nrServerTime);
179
+ if (!this.#agentRef.runtime.timeKeeper.ready) throw new Error('TimeKeeper not ready');
180
+
181
+ // If timeKeeper's origin time is ahead of nrServerTime, then the timestamp is invalid. Report a supportability metric.
182
+ const timeDiff = this.#agentRef.runtime.timeKeeper.correctedOriginTime - resp.app.nrServerTime;
183
+ if (wasReady && timeDiff > 0) {
184
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/InvalidTimestamp/Seen', timeDiff], undefined, FEATURE_NAMES.metrics, this.#agentRef.ee);
185
+ }
186
+ } catch (error) {
187
+ warn(17, error);
188
+ this.#agentRef.ee.abort();
189
+ return;
190
+ }
191
+ this.#applyConnectResponse(resp);
192
+ }
193
+ }
@@ -19,11 +19,13 @@ export class Harvester {
19
19
  constructor(agentRef) {
20
20
  this.agentRef = agentRef;
21
21
 
22
- // Create obfuscator for harvest metadata (referrer URL, etc.)
23
22
  // No event type specified - applies to all harvests regardless of feature
24
23
  this.obfuscator = new Obfuscator(agentRef);
24
+ }
25
+ startTimer(harvestInterval = this.agentRef.init.harvest.interval) {
26
+ if (this.#started) return;
27
+ this.#started = true;
25
28
  subscribeToEOL(() => {
26
- // do one last harvest round or check
27
29
  this.initializedAggregates.forEach(aggregateInst => {
28
30
  // let all features wrap up things needed to do before ANY harvest in case there's last minute cross-feature data dependencies
29
31
  if (typeof aggregateInst.harvestOpts.beforeUnload === 'function') aggregateInst.harvestOpts.beforeUnload();
@@ -31,12 +33,7 @@ export class Harvester {
31
33
  this.initializedAggregates.forEach(aggregateInst => this.triggerHarvestFor(aggregateInst, {
32
34
  isFinalHarvest: true
33
35
  }));
34
- /* This callback should run in bubble phase, so that CWV api, like "onLCP", is called before the final harvest so that emitted timings are part of last outgoing. */
35
- }, false);
36
- }
37
- startTimer(harvestInterval = this.agentRef.init.harvest.interval) {
38
- if (this.#started) return;
39
- this.#started = true;
36
+ });
40
37
  const onHarvestInterval = () => {
41
38
  this.initializedAggregates.forEach(aggregateInst => this.triggerHarvestFor(aggregateInst));
42
39
  setTimeout(onHarvestInterval, harvestInterval * 1000); // repeat in X seconds
@@ -2,7 +2,7 @@
2
2
  * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import { JSERRORS, RUM, EVENTS, FEATURE_NAMES, BLOBS, LOGS } from '../../loaders/features/features';
5
+ import { JSERRORS, RUM, EVENTS, FEATURE_NAMES, BLOBS, LOGS, CONNECT } from '../../loaders/features/features';
6
6
  import { VERSION } from "../constants/env.npm";
7
7
  import { globalScope, isWorkerScope } from '../constants/runtime';
8
8
  import { handle } from '../event-emitter/handle';
@@ -12,7 +12,7 @@ import { cleanURL } from '../url/clean-url';
12
12
  import { obj, param } from '../url/encode';
13
13
  import { warn } from '../util/console';
14
14
  import { stringify } from '../util/stringify';
15
- import { xhr as xhrMethod, xhrFetch as fetchMethod } from '../util/submit-data';
15
+ import { xhr as xhrMethod, xhrFetch as fetchMethod, getSubmitMethod } from '../util/submit-data';
16
16
  import { dispatchGlobalEvent } from '../dispatch/global-event';
17
17
 
18
18
  /**
@@ -31,7 +31,7 @@ export function send(agentRef, {
31
31
  endpoint,
32
32
  payload,
33
33
  localOpts = {},
34
- submitMethod,
34
+ submitMethod = getSubmitMethod(),
35
35
  cbFinished,
36
36
  raw,
37
37
  featureName,
@@ -52,13 +52,15 @@ export function send(agentRef, {
52
52
  }
53
53
  const protocol = agentRef.init.ssl === false ? 'http' : 'https';
54
54
  const perceivedBeacon = agentRef.init.proxy.beacon || agentRef.info.errorBeacon;
55
- const url = raw ? "".concat(protocol, "://").concat(perceivedBeacon, "/").concat(endpoint) : "".concat(protocol, "://").concat(perceivedBeacon).concat(endpoint !== RUM ? '/' + endpoint : '', "/").concat(endpointVersion, "/").concat(agentRef.info.licenseKey);
55
+ const includeEndpointName = endpoint !== RUM || endpointVersion !== 1;
56
+ const url = raw ? "".concat(protocol, "://").concat(perceivedBeacon, "/").concat(endpoint) : "".concat(protocol, "://").concat(perceivedBeacon).concat(includeEndpointName ? '/' + endpoint : '', "/").concat(endpointVersion, "/").concat(agentRef.info.licenseKey);
56
57
  const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) : '';
57
58
  let payloadParams = obj(qs, agentRef.runtime.maxBytes);
58
59
  if (baseParams === '' && payloadParams.startsWith('&')) {
59
60
  payloadParams = payloadParams.substring(1);
60
61
  }
61
- const fullUrl = "".concat(url, "?").concat(baseParams).concat(payloadParams);
62
+ const queryString = "".concat(baseParams).concat(payloadParams);
63
+ const fullUrl = queryString ? "".concat(url, "?").concat(queryString) : url; // omit the '?' if there are no query params
62
64
  const gzip = !!qs?.attributes?.includes('gzip');
63
65
 
64
66
  // all gzipped data is already in the correct format and needs no transformation
@@ -73,7 +75,7 @@ export function send(agentRef, {
73
75
  const headers = [{
74
76
  key: 'content-type',
75
77
  value: 'text/plain'
76
- }];
78
+ }, ...(localOpts.headers || [])];
77
79
 
78
80
  /* Since workers don't support sendBeacon right now, they can only use XHR method.
79
81
  Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
@@ -122,6 +124,7 @@ export function send(agentRef, {
122
124
  }
123
125
  function trackHarvestMetadata() {
124
126
  try {
127
+ if (featureName === CONNECT) return;
125
128
  if (featureName === FEATURE_NAMES.jserrors && !body?.err) return;
126
129
  const hasReplay = baseParams.includes('hr=1');
127
130
  const hasTrace = baseParams.includes('ht=1');
@@ -207,9 +210,7 @@ function baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) {
207
210
  const session = agentRef.runtime.session;
208
211
  const hr = !!session?.state.sessionReplaySentFirstChunk && session?.state.sessionReplayMode === 1 && endpoint !== JSERRORS;
209
212
  const ht = !!session?.state.traceHarvestStarted && session?.state.sessionTraceMode === 1 && ![LOGS, BLOBS].includes(endpoint);
210
- const qps = ['a=' + agentRef.info.applicationID, param('sa', agentRef.info.sa ? '' + agentRef.info.sa : ''), param('v', VERSION), transactionNameParam(), param('ct', agentRef.runtime.customTransaction), '&rst=' + now(), '&ck=0',
211
- // ck param DEPRECATED - still expected by backend
212
- '&s=' + (session?.state.value || '0'),
213
+ const qps = ['a=' + agentRef.info.applicationID, param('sa', agentRef.info.sa ? '' + agentRef.info.sa : ''), param('v', VERSION), transactionNameParam(), param('ct', agentRef.runtime.customTransaction), '&rst=' + now(), '&s=' + (session?.state.value || '0'),
213
214
  // the 0 id encaps all untrackable and default traffic
214
215
  param('ref', ref), param('ptid', agentRef.runtime.ptid ? '' + agentRef.runtime.ptid : '')];
215
216
  if (hr) qps.push(param('hr', '1', qs));
@@ -114,11 +114,11 @@ export function truncateAsString(data) {
114
114
  }
115
115
 
116
116
  /**
117
- * Creates string adder functions for BEL serialization with obfuscation and optional truncation.
117
+ * Creates string adder functions for BEL serialization with obfuscation and optional truncation, as well as raw string.
118
118
  * This ensures a single string table is used while providing separate handling for regular vs payload attributes.
119
119
  * @param {Function} getAddStringContext - Function that creates a new string table context
120
120
  * @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
121
- * @returns {{addString: Function, addStringWithTruncation: Function}} Object containing both string adder functions
121
+ * @returns {{addString: Function, addStringRaw: Function, addStringWithTruncation: Function}} Object containing various string adder functions
122
122
  */
123
123
  export function createStringAdders(getAddStringContext, obfuscator) {
124
124
  const addStringRaw = getAddStringContext();
@@ -131,6 +131,7 @@ export function createStringAdders(getAddStringContext, obfuscator) {
131
131
  };
132
132
  return {
133
133
  addString: str => processString(str, false),
134
+ addStringRaw: str => addStringRaw(str),
134
135
  addStringWithTruncation: str => processString(str, true)
135
136
  };
136
137
  }
@@ -31,19 +31,32 @@ export function getAddStringContext(obfuscator, truncator) {
31
31
  }
32
32
  }
33
33
  }
34
- export function addCustomAttributes(attrs, addString) {
34
+
35
+ /**
36
+ * Attributes are serialized according to their types, using the provided serializers. Only the first 64 attributes will be added to the payload.
37
+ * @param attrs
38
+ * @param serializers
39
+ * @param {function} serializers.addKey -function for serializing attribute keys
40
+ * @param {function} serializers.addVal - function for serializing attribute values
41
+ * @returns {*[]}
42
+ */
43
+ export function addCustomAttributes(attrs, serializers) {
44
+ const {
45
+ addKey,
46
+ addVal
47
+ } = serializers;
35
48
  var attrParts = [];
36
49
  Object.entries(attrs || {}).forEach(([key, val]) => {
37
50
  if (attrParts.length >= MAX_ATTRIBUTES) return;
38
51
  var type = 5;
39
52
  var serializedValue;
40
53
  // add key to string table first
41
- key = addString(key, false);
54
+ key = addKey(key);
42
55
  switch (typeof val) {
43
56
  case 'object':
44
57
  if (val) {
45
58
  // serialize objects to strings
46
- serializedValue = addString(stringify(val));
59
+ serializedValue = addVal(stringify(val));
47
60
  } else {
48
61
  // null attribute type
49
62
  type = 9;
@@ -62,7 +75,7 @@ export function addCustomAttributes(attrs, addString) {
62
75
  type = 9;
63
76
  break;
64
77
  default:
65
- serializedValue = addString(val);
78
+ serializedValue = addVal(val);
66
79
  }
67
80
  attrParts.push([type, key + (serializedValue ? ',' + serializedValue : '')]);
68
81
  });
@@ -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 { Timer } from './timer';
@@ -52,7 +52,7 @@ export class InteractionTimer extends Timer {
52
52
  if (state === 'hidden') this.pause();
53
53
  // vis change --> visible is treated like a new interaction with the page
54
54
  else this.resume();
55
- }, false, false, this.abortController?.signal);
55
+ }, false, this.abortController?.signal);
56
56
  }
57
57
  }
58
58
  abort() {
@@ -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 { globalScope, isWorkerScope, isBrowserScope } from '../constants/runtime';
@@ -22,9 +22,9 @@ if (isWorkerScope) {
22
22
  * This is used, for example, to submit a final harvest and send all remaining data on best-effort.
23
23
  * @param {function} cb - func to run before or during the last reliable event or time of an env's life span
24
24
  */
25
- export function subscribeToEOL(cb, capturePhase) {
25
+ export function subscribeToEOL(cb) {
26
26
  if (isBrowserScope) {
27
- subscribeToVisibilityChange(cb, true, capturePhase); // when user switches tab or hides window, esp. mobile scenario
27
+ subscribeToVisibilityChange(cb, true); // when user switches tab or hides window, esp. mobile scenario
28
28
  } else if (isWorkerScope) {
29
29
  globalScope.cleanupTasks.push(cb); // close() should run these tasks before quitting thread
30
30
  }
@@ -68,7 +68,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
68
68
  * | 50 | Failed to connect. Cannot allow registered API. |
69
69
  * | 51 | Container agent is not available to register with. Can not connect. |
70
70
  * | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
71
- * | 53 | Did not receive a valid entityGuid from connection response. |
71
+ * | 53 | Failed to parse connect response. |
72
72
  * | 54 | An experimental feature is being used. Support can not be offered for issues. |
73
73
  * | 55 | Register API has been disabled on the container agent. |
74
74
  * | 56 | Could not find a matching entity to store data. |
@@ -94,6 +94,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
94
94
  * | 76 | Agent rejected post message, could not match with existing entity. |
95
95
  * | 77 | Agent rejected post message, could not validate origin. |
96
96
  * | 78 | RegisteredIframeEntity could not determine parent origin and will not register, to avoid trusting messages from any origin. |
97
+ * | 79 | Unable to initialize Connector and/or Harvester. |
97
98
  *
98
99
  * @param {number} code The warning code to emit, which will be used to link to the warning code documentation
99
100
  * @param {*} [secondary] Secondary data to include, usually an extra message, error or object
@@ -19,7 +19,8 @@ export function activateFeatures(flags, agentRef) {
19
19
  // let any window level subscribers know that the agent is running, per install docs
20
20
  dispatchGlobalEvent({
21
21
  loaded: true,
22
- drained: true,
22
+ drained: !agentRef.init.feature_flags.includes('rum_v2'),
23
+ // v2 RUM always call activateFeatures before any group is drained (in waitForFlags)
23
24
  type: 'lifecycle',
24
25
  name: 'load',
25
26
  feature: undefined,
@@ -127,25 +127,33 @@ const observePerformance = (observers, config, onEntry) => {
127
127
  export function trackMFEVitals(target, timings) {
128
128
  let fcpObservedAt;
129
129
  let lcpObservedAt;
130
+ // Once disconnect() runs, no further vitals data should be written - even from an observer callback that was
131
+ // already in flight when disconnect() was called.
132
+ let disconnected = false;
130
133
  const getTimeRelativeToScriptStart = capturedAt => {
131
134
  if (capturedAt == null) return capturedAt;
132
135
  return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
133
136
  };
134
137
  const vitals = {
135
138
  fcp: {
139
+ // A clock freeze/tab suspend between script registration and first paint can make this value huge once the
140
+ // tab resumes. Rather than report a multi-hour "FCP", drop it so bad data doesn't reach the backend.
136
141
  get value() {
137
- return getTimeRelativeToScriptStart(fcpObservedAt);
142
+ const relative = getTimeRelativeToScriptStart(fcpObservedAt);
143
+ return relative > CORRELATION_STALE_THRESHOLD_MS ? undefined : relative;
138
144
  },
139
145
  set value(v) {
140
- fcpObservedAt = v;
146
+ if (!disconnected) fcpObservedAt = v;
141
147
  }
142
148
  },
143
149
  lcp: {
150
+ // LCP can never occur before FCP, so if FCP was dropped as invalid, LCP is derived from the same untrustworthy clock and must be dropped too.
144
151
  get value() {
152
+ if (vitals.fcp.value === undefined) return undefined;
145
153
  return getTimeRelativeToScriptStart(lcpObservedAt);
146
154
  },
147
155
  set value(v) {
148
- lcpObservedAt = v;
156
+ if (!disconnected) lcpObservedAt = v;
149
157
  }
150
158
  },
151
159
  cls: {
@@ -165,15 +173,22 @@ export function trackMFEVitals(target, timings) {
165
173
  if (!fcpObservedAt) vitals.disconnect();
166
174
  }, CORRELATION_STALE_THRESHOLD_MS);
167
175
  const populateVitalMinimums = () => {
168
- fcpObservedAt ??= now();
176
+ if (disconnected) return;
177
+ if (fcpObservedAt == null) {
178
+ const capturedAt = now();
179
+ // If the very first observed render already exceeds the window, the clock is untrustworthy for this
180
+ // MFE (e.g. tab was frozen/suspended between registration and paint) - shut everything down instead
181
+ // of seeding vitals with data derived from it.
182
+ if (getTimeRelativeToScriptStart(capturedAt) > CORRELATION_STALE_THRESHOLD_MS) {
183
+ vitals.disconnect();
184
+ return;
185
+ }
186
+ fcpObservedAt = capturedAt;
187
+ }
169
188
  lcpObservedAt ??= now();
170
189
  vitals.cls.value ??= 0;
171
190
  };
172
191
 
173
- // if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
174
- const existingRoots = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
175
- if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
176
-
177
192
  // Track FCP - first contentful paint
178
193
  const fcpObs = observeMutations(target, (_, obs) => {
179
194
  // An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
@@ -188,12 +203,13 @@ export function trackMFEVitals(target, timings) {
188
203
  const observedElements = new Set();
189
204
  try {
190
205
  resizeObs = new globalScope.ResizeObserver(entries => {
206
+ if (disconnected) return;
191
207
  entries.forEach(entry => {
192
208
  try {
193
209
  const size = entry.contentRect.width * entry.contentRect.height;
194
210
  if (size > largestSize) {
195
211
  largestSize = size;
196
- lcpObservedAt = now();
212
+ vitals.lcp.value = now();
197
213
  }
198
214
  resizeObs.unobserve(entry.target);
199
215
  } catch (e) {
@@ -254,7 +270,7 @@ export function trackMFEVitals(target, timings) {
254
270
  type: 'layout-shift',
255
271
  buffered: true
256
272
  }, entry => {
257
- if (entry.hadRecentInput) return;
273
+ if (disconnected || entry.hadRecentInput) return;
258
274
  (entry.sources || []).some(source => {
259
275
  if (isInMFE(source.node, target)) {
260
276
  // an observed "CLS" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
@@ -272,7 +288,7 @@ export function trackMFEVitals(target, timings) {
272
288
  buffered: true,
273
289
  durationThreshold: 40
274
290
  }, entry => {
275
- if (!entry.interactionId || !isInMFE(entry.target, target)) return;
291
+ if (disconnected || !entry.interactionId || !isInMFE(entry.target, target)) return;
276
292
  if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
277
293
  // an observed "INP" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
278
294
  populateVitalMinimums();
@@ -290,6 +306,7 @@ export function trackMFEVitals(target, timings) {
290
306
 
291
307
  // Disconnect all observers
292
308
  vitals.disconnect = () => {
309
+ disconnected = true;
293
310
  // Disconnect all observers
294
311
  observers.forEach(obs => {
295
312
  try {
@@ -336,5 +353,11 @@ export function trackMFEVitals(target, timings) {
336
353
  passive: true
337
354
  });
338
355
  });
356
+
357
+ // If the MFE has already rendered something on the page before we could set up listeners, just populate vital
358
+ // minimums immediately. Done last, once `observers` is fully populated and `vitals.disconnect` has its real
359
+ // implementation, so a stale-clock detection here can actually tear everything down instead of no-op'ing.
360
+ const existingRoots = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
361
+ if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
339
362
  return vitals;
340
363
  }
@@ -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
 
@@ -145,6 +145,7 @@ export class Aggregate extends AggregateBase {
145
145
  if (!eventBuffer.length) return;
146
146
  const {
147
147
  addString,
148
+ addStringRaw,
148
149
  addStringWithTruncation
149
150
  } = createStringAdders(getAddStringContext, this.obfuscator);
150
151
  let payload = 'bel.7;';
@@ -158,11 +159,11 @@ export class Aggregate extends AggregateBase {
158
159
  // callbackEnd
159
160
  numeric(0),
160
161
  // no callbackDuration for non-SPA events
161
- addString(event.method), numeric(event.status), addString(event.domain), addString(event.path), numeric(event.requestSize), numeric(event.responseSize), event.type === 'fetch' ? 1 : '', addString(0),
162
+ addStringRaw(event.method), numeric(event.status), addString(event.domain), addString(event.path), numeric(event.requestSize), numeric(event.responseSize), event.type === 'fetch' ? 1 : '', addStringRaw(0),
162
163
  // nodeId
163
- nullable(event.spanId, addString, true) +
164
+ nullable(event.spanId, addStringRaw, true) +
164
165
  // guid
165
- nullable(event.traceId, addString, true) +
166
+ nullable(event.traceId, addStringRaw, true) +
166
167
  // traceId
167
168
  nullable(event.spanTimestamp, numeric, false) // timestamp
168
169
  ];
@@ -172,14 +173,21 @@ export class Aggregate extends AggregateBase {
172
173
  const jsAttributes = this.agentRef.info.jsAttributes;
173
174
 
174
175
  // Regular attributes: obfuscate only
175
- const regularAttrs = addCustomAttributes({
176
+ let regularAttrs = addCustomAttributes({
176
177
  ...(jsAttributes || {}),
177
- [AJAX_ID]: event[AJAX_ID],
178
- // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
179
178
  ...(event.targetAttributes || {}),
180
179
  // used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
181
180
  ...(event.gql || {})
182
- }, addString);
181
+ }, {
182
+ addKey: addStringRaw,
183
+ addVal: addString
184
+ });
185
+ regularAttrs.push(...addCustomAttributes({
186
+ [AJAX_ID]: event[AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
187
+ }, {
188
+ addKey: addStringRaw,
189
+ addVal: addStringRaw
190
+ }));
183
191
 
184
192
  // Payload attributes: obfuscate then truncate
185
193
  const payloadAttrs = addCustomAttributes({
@@ -198,7 +206,10 @@ export class Aggregate extends AggregateBase {
198
206
  ...(event.responseHeaders ? {
199
207
  responseHeaders: event.responseHeaders
200
208
  } : {})
201
- }, addStringWithTruncation);
209
+ }, {
210
+ addKey: addStringRaw,
211
+ addVal: addStringWithTruncation
212
+ });
202
213
  const attrParts = [...regularAttrs, ...payloadAttrs];
203
214
  fields.unshift(numeric(attrParts.length));
204
215
  insert += fields.join(',');
@@ -132,9 +132,7 @@ export class Aggregate extends AggregateBase {
132
132
  return queryParameters;
133
133
  };
134
134
  this.events.add(body);
135
- if (this.agentRef.runtime.harvester.triggerHarvestFor(this, {
136
- sendEmptyBody: true
137
- }).ranSend) this.sentRum = true;
135
+ if (this.agentRef.runtime.harvester.triggerHarvestFor(this).ranSend) this.sentRum = true;
138
136
  }
139
137
  serializer(eventBuffer) {
140
138
  // this is necessary because PVE sends a single item rather than an array; in the case of undefined, this prevents sending [null] as body
@@ -175,9 +173,7 @@ export class Aggregate extends AggregateBase {
175
173
 
176
174
  if (this.isRetrying && this.retries++ < 1) {
177
175
  // Only retry once
178
- setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this, {
179
- sendEmptyBody: true
180
- }), 5000); // Retry sending the RUM event after 5 seconds
176
+ setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this), 5000); // Retry sending the RUM event after 5 seconds
181
177
  return;
182
178
  }
183
179
  if (status >= 400 || status === 0) {