@newrelic/browser-agent 1.316.0 → 1.317.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/common/config/configurable.js +1 -1
  3. package/dist/cjs/common/config/init-types.js +1 -0
  4. package/dist/cjs/common/config/init.js +6 -4
  5. package/dist/cjs/common/constants/env.cdn.js +2 -2
  6. package/dist/cjs/common/constants/env.npm.js +2 -2
  7. package/dist/cjs/common/constants/events.js +26 -0
  8. package/dist/cjs/common/harvest/harvester.js +9 -217
  9. package/dist/cjs/common/harvest/send.js +232 -0
  10. package/dist/cjs/common/harvest/types.js +9 -5
  11. package/dist/cjs/common/payloads/payloads.js +127 -0
  12. package/dist/cjs/common/serialize/bel-serializer.js +4 -3
  13. package/dist/cjs/common/session/session-entity.js +8 -8
  14. package/dist/cjs/common/util/data-size.js +2 -2
  15. package/dist/cjs/common/util/obfuscate.js +147 -3
  16. package/dist/cjs/common/v2/mfe-vitals.js +286 -0
  17. package/dist/cjs/common/wrap/wrap-websocket.js +2 -1
  18. package/dist/cjs/common/wrap/wrap-xhr.js +1 -1
  19. package/dist/cjs/features/ajax/aggregate/gql.js +40 -1
  20. package/dist/cjs/features/ajax/aggregate/index.js +51 -13
  21. package/dist/cjs/features/ajax/constants.js +6 -1
  22. package/dist/cjs/features/ajax/instrument/index.js +105 -17
  23. package/dist/cjs/features/generic_events/aggregate/index.js +15 -10
  24. package/dist/cjs/features/generic_events/constants.js +3 -2
  25. package/dist/cjs/features/jserrors/aggregate/format-stack-trace.js +2 -2
  26. package/dist/cjs/features/jserrors/aggregate/index.js +12 -3
  27. package/dist/cjs/features/logging/aggregate/index.js +12 -8
  28. package/dist/cjs/features/metrics/aggregate/index.js +5 -1
  29. package/dist/cjs/features/page_view_event/aggregate/index.js +72 -33
  30. package/dist/cjs/features/page_view_timing/aggregate/index.js +6 -1
  31. package/dist/cjs/features/session_replay/aggregate/index.js +39 -29
  32. package/dist/cjs/features/session_trace/aggregate/index.js +35 -29
  33. package/dist/cjs/features/session_trace/constants.js +17 -3
  34. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +36 -5
  35. package/dist/cjs/features/soft_navigations/aggregate/index.js +7 -1
  36. package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  37. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +6 -4
  38. package/dist/cjs/features/utils/aggregate-base.js +4 -3
  39. package/dist/cjs/loaders/api/register.js +26 -4
  40. package/dist/esm/common/config/configurable.js +1 -1
  41. package/dist/esm/common/config/init-types.js +1 -0
  42. package/dist/esm/common/config/init.js +3 -1
  43. package/dist/esm/common/constants/env.cdn.js +2 -2
  44. package/dist/esm/common/constants/env.npm.js +2 -2
  45. package/dist/esm/common/constants/events.js +20 -0
  46. package/dist/esm/common/harvest/harvester.js +9 -217
  47. package/dist/esm/common/harvest/send.js +226 -0
  48. package/dist/esm/common/harvest/types.js +9 -5
  49. package/dist/esm/common/payloads/payloads.js +118 -0
  50. package/dist/esm/common/serialize/bel-serializer.js +4 -3
  51. package/dist/esm/common/session/session-entity.js +9 -9
  52. package/dist/esm/common/util/data-size.js +2 -2
  53. package/dist/esm/common/util/obfuscate.js +147 -3
  54. package/dist/esm/common/v2/mfe-vitals.js +281 -0
  55. package/dist/esm/common/wrap/wrap-websocket.js +2 -1
  56. package/dist/esm/common/wrap/wrap-xhr.js +1 -1
  57. package/dist/esm/features/ajax/aggregate/gql.js +39 -1
  58. package/dist/esm/features/ajax/aggregate/index.js +52 -14
  59. package/dist/esm/features/ajax/constants.js +5 -0
  60. package/dist/esm/features/ajax/instrument/index.js +106 -18
  61. package/dist/esm/features/generic_events/aggregate/index.js +15 -10
  62. package/dist/esm/features/generic_events/constants.js +3 -2
  63. package/dist/esm/features/jserrors/aggregate/format-stack-trace.js +2 -2
  64. package/dist/esm/features/jserrors/aggregate/index.js +12 -3
  65. package/dist/esm/features/logging/aggregate/index.js +12 -8
  66. package/dist/esm/features/metrics/aggregate/index.js +5 -1
  67. package/dist/esm/features/page_view_event/aggregate/index.js +71 -32
  68. package/dist/esm/features/page_view_timing/aggregate/index.js +6 -1
  69. package/dist/esm/features/session_replay/aggregate/index.js +39 -29
  70. package/dist/esm/features/session_trace/aggregate/index.js +36 -30
  71. package/dist/esm/features/session_trace/constants.js +16 -2
  72. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +36 -5
  73. package/dist/esm/features/soft_navigations/aggregate/index.js +7 -1
  74. package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  75. package/dist/esm/features/soft_navigations/aggregate/interaction.js +6 -4
  76. package/dist/esm/features/utils/aggregate-base.js +4 -3
  77. package/dist/esm/loaders/api/register.js +26 -4
  78. package/dist/tsconfig.tsbuildinfo +1 -1
  79. package/dist/types/common/config/init-types.d.ts +4 -0
  80. package/dist/types/common/config/init-types.d.ts.map +1 -1
  81. package/dist/types/common/config/init.d.ts.map +1 -1
  82. package/dist/types/common/constants/events.d.ts +16 -0
  83. package/dist/types/common/constants/events.d.ts.map +1 -0
  84. package/dist/types/common/harvest/harvester.d.ts +2 -16
  85. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  86. package/dist/types/common/harvest/send.d.ts +9 -0
  87. package/dist/types/common/harvest/send.d.ts.map +1 -0
  88. package/dist/types/common/harvest/types.d.ts +39 -10
  89. package/dist/types/common/harvest/types.d.ts.map +1 -1
  90. package/dist/types/common/payloads/payloads.d.ts +42 -0
  91. package/dist/types/common/payloads/payloads.d.ts.map +1 -0
  92. package/dist/types/common/serialize/bel-serializer.d.ts +1 -1
  93. package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
  94. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  95. package/dist/types/common/util/obfuscate.d.ts +23 -2
  96. package/dist/types/common/util/obfuscate.d.ts.map +1 -1
  97. package/dist/types/common/v2/mfe-vitals.d.ts +14 -0
  98. package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -0
  99. package/dist/types/common/wrap/wrap-websocket.d.ts.map +1 -1
  100. package/dist/types/features/ajax/aggregate/gql.d.ts +8 -0
  101. package/dist/types/features/ajax/aggregate/gql.d.ts.map +1 -1
  102. package/dist/types/features/ajax/aggregate/index.d.ts +2 -0
  103. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  104. package/dist/types/features/ajax/constants.d.ts +5 -0
  105. package/dist/types/features/ajax/constants.d.ts.map +1 -1
  106. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  107. package/dist/types/features/generic_events/aggregate/index.d.ts +3 -1
  108. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  109. package/dist/types/features/generic_events/constants.d.ts.map +1 -1
  110. package/dist/types/features/jserrors/aggregate/index.d.ts +8 -1
  111. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  112. package/dist/types/features/logging/aggregate/index.d.ts +3 -1
  113. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  114. package/dist/types/features/metrics/aggregate/index.d.ts +2 -0
  115. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  116. package/dist/types/features/page_view_event/aggregate/index.d.ts +3 -0
  117. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  118. package/dist/types/features/page_view_timing/aggregate/index.d.ts +2 -0
  119. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  120. package/dist/types/features/session_replay/aggregate/index.d.ts +4 -2
  121. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  122. package/dist/types/features/session_trace/aggregate/index.d.ts +3 -1
  123. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  124. package/dist/types/features/session_trace/constants.d.ts +17 -0
  125. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +6 -1
  126. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  127. package/dist/types/features/soft_navigations/aggregate/index.d.ts +3 -0
  128. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  129. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +3 -1
  130. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  131. package/dist/types/features/utils/aggregate-base.d.ts +0 -1
  132. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  133. package/dist/types/loaders/api/register.d.ts.map +1 -1
  134. package/package.json +3 -3
  135. package/src/common/config/configurable.js +1 -1
  136. package/src/common/config/init-types.js +1 -0
  137. package/src/common/config/init.js +2 -1
  138. package/src/common/constants/events.js +20 -0
  139. package/src/common/harvest/harvester.js +9 -201
  140. package/src/common/harvest/send.js +209 -0
  141. package/src/common/harvest/types.js +9 -5
  142. package/src/common/payloads/payloads.js +135 -0
  143. package/src/common/serialize/bel-serializer.js +4 -3
  144. package/src/common/session/session-entity.js +8 -9
  145. package/src/common/util/data-size.js +2 -2
  146. package/src/common/util/obfuscate.js +154 -3
  147. package/src/common/v2/mfe-vitals.js +260 -0
  148. package/src/common/wrap/wrap-websocket.js +2 -1
  149. package/src/common/wrap/wrap-xhr.js +1 -1
  150. package/src/features/ajax/aggregate/gql.js +42 -1
  151. package/src/features/ajax/aggregate/index.js +43 -13
  152. package/src/features/ajax/constants.js +5 -1
  153. package/src/features/ajax/instrument/index.js +114 -18
  154. package/src/features/generic_events/aggregate/index.js +14 -9
  155. package/src/features/generic_events/constants.js +3 -2
  156. package/src/features/jserrors/aggregate/format-stack-trace.js +3 -3
  157. package/src/features/jserrors/aggregate/index.js +12 -3
  158. package/src/features/logging/aggregate/index.js +12 -11
  159. package/src/features/metrics/aggregate/index.js +5 -1
  160. package/src/features/page_view_event/aggregate/index.js +58 -29
  161. package/src/features/page_view_timing/aggregate/index.js +6 -1
  162. package/src/features/session_replay/aggregate/index.js +37 -28
  163. package/src/features/session_trace/aggregate/index.js +37 -30
  164. package/src/features/session_trace/constants.js +7 -1
  165. package/src/features/soft_navigations/aggregate/ajax-node.js +26 -5
  166. package/src/features/soft_navigations/aggregate/index.js +7 -1
  167. package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +1 -1
  168. package/src/features/soft_navigations/aggregate/interaction.js +6 -4
  169. package/src/features/utils/aggregate-base.js +3 -3
  170. package/src/loaders/api/register.js +24 -4
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import { JSERRORS, RUM, EVENTS, FEATURE_NAMES, BLOBS, LOGS } from '../../loaders/features/features';
6
+ import { VERSION } from "../constants/env.npm";
7
+ import { globalScope, isWorkerScope } from '../constants/runtime';
8
+ import { handle } from '../event-emitter/handle';
9
+ import { eventListenerOpts } from '../event-listener/event-listener-opts';
10
+ import { now } from '../timing/now';
11
+ import { cleanURL } from '../url/clean-url';
12
+ import { obj, param } from '../url/encode';
13
+ import { warn } from '../util/console';
14
+ import { stringify } from '../util/stringify';
15
+ import { xhr as xhrMethod, xhrFetch as fetchMethod } from '../util/submit-data';
16
+ import { dispatchGlobalEvent } from '../dispatch/global-event';
17
+
18
+ /**
19
+ * @typedef {import('./types.js').NetworkSendSpec} NetworkSendSpec
20
+ */
21
+
22
+ const warnings = {};
23
+
24
+ /**
25
+ * Initiate a harvest call.
26
+ * @param {object} agentRef The initialized agent reference
27
+ * @param {NetworkSendSpec} spec Specification for sending data
28
+ * @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
29
+ */
30
+ export function send(agentRef, {
31
+ endpoint,
32
+ payload,
33
+ localOpts = {},
34
+ submitMethod,
35
+ cbFinished,
36
+ raw,
37
+ featureName,
38
+ endpointVersion = 1,
39
+ harvesterObfuscator
40
+ }) {
41
+ if (!agentRef.info.errorBeacon) return false;
42
+ let {
43
+ body,
44
+ qs
45
+ } = cleanPayload(payload);
46
+ if (Object.keys(body).length === 0 && !localOpts.sendEmptyBody) {
47
+ // if there's no body to send, just run onfinish stuff and return
48
+ if (cbFinished) cbFinished({
49
+ sent: false
50
+ });
51
+ return false;
52
+ }
53
+ const protocol = agentRef.init.ssl === false ? 'http' : 'https';
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);
56
+ const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) : '';
57
+ let payloadParams = obj(qs, agentRef.runtime.maxBytes);
58
+ if (baseParams === '' && payloadParams.startsWith('&')) {
59
+ payloadParams = payloadParams.substring(1);
60
+ }
61
+ const fullUrl = "".concat(url, "?").concat(baseParams).concat(payloadParams);
62
+ const gzip = !!qs?.attributes?.includes('gzip');
63
+
64
+ // all gzipped data is already in the correct format and needs no transformation
65
+ // all features going to 'events' endpoint should already be serialized & stringified
66
+ let stringBody = gzip || endpoint === EVENTS ? body : stringify(body);
67
+
68
+ // If body is null, undefined, or an empty object or array after stringifying, send an empty string instead.
69
+ if (!stringBody || stringBody.length === 0 || stringBody === '{}' || stringBody === '[]') stringBody = '';
70
+
71
+ // Warn--once per endpoint--if the agent tries to send large payloads
72
+ if (endpoint !== BLOBS && stringBody.length > 750000 && (warnings[endpoint] = (warnings[endpoint] || 0) + 1) === 1) warn(28, endpoint);
73
+ const headers = [{
74
+ key: 'content-type',
75
+ value: 'text/plain'
76
+ }];
77
+
78
+ /* Since workers don't support sendBeacon right now, they can only use XHR method.
79
+ Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
80
+ we just make a BLOCKING request--trivial impact--with the remaining data as a temp fill-in for sendBeacon.
81
+ Following the removal of img-element method. */
82
+ let result = submitMethod({
83
+ url: fullUrl,
84
+ body: stringBody,
85
+ sync: localOpts.isFinalHarvest && isWorkerScope,
86
+ headers
87
+ });
88
+ if (!localOpts.isFinalHarvest && cbFinished) {
89
+ // final harvests don't hold onto buffer data (shouldRetryOnFail is false), so cleanup isn't needed
90
+ if (submitMethod === xhrMethod) {
91
+ result.addEventListener('loadend', function () {
92
+ // `this` here in block refers to the XHR object in this scope, do not change the anon function to an arrow function
93
+ // status 0 refers to a local error, such as CORS or network failure, or a blocked request by the browser (e.g. adblocker)
94
+ const cbResult = {
95
+ sent: this.status !== 0,
96
+ status: this.status,
97
+ retry: shouldRetry(this.status),
98
+ fullUrl,
99
+ xhr: this,
100
+ responseText: this.responseText
101
+ };
102
+ cbFinished(cbResult);
103
+
104
+ /** temporary audit of consistency of harvest metadata flags */
105
+ if (!shouldRetry(this.status)) trackHarvestMetadata();
106
+ }, eventListenerOpts(false));
107
+ } else if (submitMethod === fetchMethod) {
108
+ result.then(async function (response) {
109
+ const status = response.status;
110
+ const cbResult = {
111
+ sent: true,
112
+ status,
113
+ retry: shouldRetry(status),
114
+ fullUrl,
115
+ fetchResponse: response,
116
+ responseText: await response.text()
117
+ };
118
+ cbFinished(cbResult);
119
+ /** temporary audit of consistency of harvest metadata flags */
120
+ if (!shouldRetry(status)) trackHarvestMetadata();
121
+ });
122
+ }
123
+ function trackHarvestMetadata() {
124
+ try {
125
+ if (featureName === FEATURE_NAMES.jserrors && !body?.err) return;
126
+ const hasReplay = baseParams.includes('hr=1');
127
+ const hasTrace = baseParams.includes('ht=1');
128
+ const hasError = qs?.attributes?.includes('hasError=true');
129
+ handle('harvest-metadata', [{
130
+ [featureName]: {
131
+ ...(hasReplay && {
132
+ hasReplay
133
+ }),
134
+ ...(hasTrace && {
135
+ hasTrace
136
+ }),
137
+ ...(hasError && {
138
+ hasError
139
+ })
140
+ }
141
+ }], undefined, FEATURE_NAMES.metrics, agentRef.ee);
142
+ } catch (err) {
143
+ // do nothing
144
+ }
145
+ }
146
+ }
147
+ dispatchGlobalEvent({
148
+ drained: !!agentRef.runtime?.activatedFeatures,
149
+ type: 'data',
150
+ name: 'harvest',
151
+ feature: featureName,
152
+ data: {
153
+ endpoint,
154
+ headers,
155
+ payload,
156
+ submitMethod: getSubmitMethodName(),
157
+ raw,
158
+ synchronousXhr: !!(localOpts.isFinalHarvest && isWorkerScope)
159
+ }
160
+ });
161
+ return true;
162
+ function shouldRetry(status) {
163
+ switch (status) {
164
+ case 408:
165
+ case 429:
166
+ case 500:
167
+ return true;
168
+ }
169
+ return status >= 502 && status <= 504 || status >= 512 && status <= 530;
170
+ }
171
+ function getSubmitMethodName() {
172
+ if (submitMethod === xhrMethod) return 'xhr';
173
+ if (submitMethod === fetchMethod) return 'fetch';
174
+ return 'beacon';
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Cleans and returns a payload object containing a body and qs
180
+ * object with key/value pairs. KV pairs where the value is null,
181
+ * undefined, or an empty string are removed to save on transmission
182
+ * size.
183
+ * @param {HarvestPayload} payload Payload to be sent to the endpoint.
184
+ * @returns {HarvestPayload} Cleaned payload payload to be sent to the endpoint.
185
+ */
186
+ function cleanPayload(payload = {}) {
187
+ const clean = input => {
188
+ if (typeof Uint8Array !== 'undefined' && input instanceof Uint8Array || Array.isArray(input)) return input;
189
+ if (typeof input === 'string') return input;
190
+ return Object.entries(input || {}).reduce((accumulator, [key, value]) => {
191
+ if (typeof value === 'number' || typeof value === 'string' && value.length > 0 || typeof value === 'object' && Object.keys(value || {}).length > 0) {
192
+ accumulator[key] = value;
193
+ }
194
+ return accumulator;
195
+ }, {});
196
+ };
197
+ return {
198
+ body: clean(payload.body),
199
+ qs: clean(payload.qs)
200
+ };
201
+ }
202
+
203
+ // The stuff that gets sent every time.
204
+ function baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) {
205
+ const cleanedURL = cleanURL('' + globalScope.location);
206
+ const ref = harvesterObfuscator?.obfuscateString(cleanedURL) ?? cleanedURL;
207
+ const session = agentRef.runtime.session;
208
+ const hr = !!session?.state.sessionReplaySentFirstChunk && session?.state.sessionReplayMode === 1 && endpoint !== JSERRORS;
209
+ 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
+ // the 0 id encaps all untrackable and default traffic
214
+ param('ref', ref), param('ptid', agentRef.runtime.ptid ? '' + agentRef.runtime.ptid : '')];
215
+ if (hr) qps.push(param('hr', '1', qs));
216
+ if (ht) qps.push(param('ht', '1', qs));
217
+ return qps.join('');
218
+
219
+ // Constructs the transaction name param for the beacon URL.
220
+ // Prefers the obfuscated transaction name over the plain text.
221
+ // Falls back to making up a name.
222
+ function transactionNameParam() {
223
+ if (agentRef.info.transactionName) return param('to', agentRef.info.transactionName);
224
+ return param('t', agentRef.info.tNamePlain || 'Unnamed Transaction');
225
+ }
226
+ }
@@ -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
 
@@ -23,11 +23,15 @@
23
23
  * @typedef {object} NetworkSendSpec
24
24
  * @property {HarvestEndpointIdentifier} endpoint The endpoint to use (jserrors, events, resources etc.)
25
25
  * @property {HarvestPayload} payload Object representing payload.
26
- * @property {object} localOpts Additional options for sending data
27
- * @property {boolean} localOpts.isFinalHarvest Specify whether the call is a final harvest during page unload.
28
- * @property {boolean} localOpts.sendEmptyBody Specify whether the call should be made even if the body is empty. Useful for rum calls.
29
- * @property {boolean} localOpts.forceNoRetry Don't save the buffered data in the case of a need to retry the transmission.
26
+ * @property {object} [localOpts] Additional options for sending data
27
+ * @property {boolean} [localOpts.isFinalHarvest] Specify whether the call is a final harvest during page unload.
28
+ * @property {boolean} [localOpts.sendEmptyBody] Specify whether the call should be made even if the body is empty. Useful for rum calls.
29
+ * @property {boolean} [localOpts.forceNoRetry] Don't save the buffered data in the case of a need to retry the transmission.
30
30
  * @property {import('../util/submit-data.js').NetworkMethods} submitMethod The network method to use {@link ../util/submit-data.js}
31
+ * @property {function(object): void} [cbFinished] Callback invoked with the result once the network request completes. Receives an object with sent, status, retry, fullUrl, responseText, and either xhr or fetchResponse.
32
+ * @property {boolean} [raw] If true, skips standard versioned URL path construction and uses a bare endpoint URL.
33
+ * @property {string} [featureName] The feature name associated with this harvest, used for metadata tracking and the global harvest event.
34
+ * @property {number} [endpointVersion] The endpoint version included in the URL path. Defaults to 1.
31
35
  */
32
36
 
33
37
  export const unused = {};
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Copyright 2020-2026 New Relic, Inc. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { stringify } from '../util/stringify';
7
+ import { CAPTURE_PAYLOAD_SETTINGS } from '../../features/ajax/constants';
8
+
9
+ /**
10
+ * Determines whether payload data should be captured based on the capture mode setting,
11
+ * HTTP status code, and GraphQL error status.
12
+ * @param {string} captureMode - The capture mode setting ('none', 'all', or 'failures')
13
+ * @param {number} statusCode - The HTTP status code
14
+ * @param {boolean} hasGQLErrors - Whether the response contains GraphQL errors
15
+ * @returns {boolean} True if payload should be captured
16
+ */
17
+ export function canCapturePayload(captureMode, statusCode, hasGQLErrors) {
18
+ if (captureMode === CAPTURE_PAYLOAD_SETTINGS.ALL) return true;
19
+ if (!captureMode || captureMode === CAPTURE_PAYLOAD_SETTINGS.NONE) return false;
20
+
21
+ // Default "failures" mode
22
+ return statusCode === 0 || statusCode >= 400 || hasGQLErrors === true;
23
+ }
24
+
25
+ /**
26
+ * Parses a query string into an object of key-value pairs.
27
+ * @param {string} search a query string starting with "?" or without (ex. new URL(...).search)
28
+ * @returns {Object|undefined} Parsed query parameters as key-value pairs. Returns undefined if no valid parameters are found.
29
+ */
30
+ export function parseQueryString(search) {
31
+ if (!search || search.length === 0) return;
32
+ const queryParams = {};
33
+ try {
34
+ const searchParams = new URLSearchParams(search);
35
+ searchParams.forEach(function (value, key) {
36
+ queryParams[key] = value;
37
+ });
38
+ } catch (e) {
39
+ // Fallback for environments without URLSearchParams
40
+ }
41
+ return queryParams;
42
+ }
43
+
44
+ /**
45
+ * Determines if the given content type is likely to be human-readable (text-based).
46
+ * @param {Object} headers - The headers object containing content-type
47
+ * @param {*} data - The data to check
48
+ * @returns {boolean} True if the content type is human-readable (text-based)
49
+ */
50
+ export function isLikelyHumanReadable(headers, data) {
51
+ if (!headers) return typeof data === 'string';
52
+ var contentType = headers['content-type'];
53
+ if (!contentType) return typeof data === 'string';
54
+ // Normalize to lowercase and extract the mime type (ignore charset, etc.)
55
+ var mimeType = contentType.toLowerCase().split(';')[0].trim();
56
+
57
+ // Check for text/* types
58
+ if (mimeType.indexOf('text/') === 0) return true;
59
+
60
+ // Check for specific application/* types
61
+ var readableAppTypes = ['/json', '/xml', '/xhtml+xml', '/ld+json', '/yaml', '/x-www-form-urlencoded'];
62
+ for (var i = 0; i < readableAppTypes.length; i++) {
63
+ if (mimeType === 'application' + readableAppTypes[i]) return true;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * Truncates a string to ensure its UTF-8 byte length does not exceed 4092 bytes. If truncation is necessary,
70
+ * the string is cut off at a character boundary to avoid breaking multi-byte characters and " ..." is appended to indicate truncation.
71
+ * If not a string, it is first converted to a string using JSON.stringify.
72
+ * @param {*} data The data to truncate.
73
+ * @returns {string}
74
+ */
75
+ export function truncateAsString(data) {
76
+ if (!data) return data;
77
+ try {
78
+ if (typeof data !== 'string') data = stringify(data);
79
+ let bytes = 0;
80
+ let i = 0;
81
+ let needsEllipsis = false;
82
+ while (i < data.length) {
83
+ const c = data.charCodeAt(i);
84
+ const charBytes = c < 0x80 ? 1 : c < 0x800 ? 2 : c < 0xD800 || c >= 0xE000 ? 3 : 4;
85
+ if (bytes + charBytes > 4092) {
86
+ needsEllipsis = true;
87
+ break;
88
+ }
89
+ bytes += charBytes;
90
+ i += charBytes === 4 ? 2 : 1;
91
+ }
92
+ return data.slice(0, i) + (needsEllipsis ? ' ...' : '');
93
+ } catch (e) {
94
+ return data;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Creates string adder functions for BEL serialization with obfuscation and optional truncation.
100
+ * This ensures a single string table is used while providing separate handling for regular vs payload attributes.
101
+ * @param {Function} getAddStringContext - Function that creates a new string table context
102
+ * @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
103
+ * @returns {{addString: Function, addStringWithTruncation: Function}} Object containing both string adder functions
104
+ */
105
+ export function createStringAdders(getAddStringContext, obfuscator) {
106
+ const addStringRaw = getAddStringContext();
107
+ const processString = (str, shouldTruncate) => {
108
+ if (typeof str === 'undefined' || str === '') return addStringRaw(str);
109
+ if (typeof str !== 'string') str = stringify(str);
110
+ const obfuscated = obfuscator?.obfuscateString(str) ?? str;
111
+ const processed = shouldTruncate ? truncateAsString(obfuscated) : obfuscated;
112
+ return addStringRaw(processed);
113
+ };
114
+ return {
115
+ addString: str => processString(str, false),
116
+ addStringWithTruncation: str => processString(str, true)
117
+ };
118
+ }
@@ -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
 
@@ -15,13 +15,14 @@ export function numeric(n, noDefault) {
15
15
  }
16
16
  return n === undefined || n === 0 ? '' : Math.floor(n).toString(36);
17
17
  }
18
- export function getAddStringContext(obfuscator) {
18
+ export function getAddStringContext(obfuscator, truncator) {
19
19
  let stringTableIdx = 0;
20
20
  const stringTable = Object.prototype.hasOwnProperty.call(Object, 'create') ? Object.create(null) : {};
21
21
  return addString;
22
22
  function addString(str) {
23
23
  if (typeof str === 'undefined' || str === '') return '';
24
- str = obfuscator.obfuscateString(String(str));
24
+ str = obfuscator?.obfuscateString(String(str)) ?? String(str);
25
+ str = truncator?.(str) ?? str;
25
26
  if (hasOwnProp.call(stringTable, str)) {
26
27
  return numeric(stringTable[str], true);
27
28
  } else {
@@ -7,7 +7,7 @@ import { warn } from '../util/console';
7
7
  import { stringify } from '../util/stringify';
8
8
  import { Timer } from '../timer/timer';
9
9
  import { isBrowserScope } from '../constants/runtime';
10
- import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS, MODE, SESSION_EVENTS, SESSION_EVENT_TYPES, SESSION_STORAGE_KEY_PREFIX } from './constants';
10
+ import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS, SESSION_EVENTS, SESSION_EVENT_TYPES, SESSION_STORAGE_KEY_PREFIX } from './constants';
11
11
  import { InteractionTimer } from '../timer/interaction-timer';
12
12
  import { wrapEvents } from '../wrap/wrap-events';
13
13
  import { getModeledObject } from '../config/configurable';
@@ -15,26 +15,27 @@ import { handle } from '../event-emitter/handle';
15
15
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants';
16
16
  import { FEATURE_NAMES } from '../../loaders/features/features';
17
17
  import { windowAddEventListener } from '../event-listener/event-listener-opts';
18
- import { LOGGING_MODE } from '../../features/logging/constants';
19
18
 
20
- // this is what can be stored in local storage (not enforced but probably should be)
19
+ // this is what can be stored in local storage (enforced during reads)
21
20
  // these values should sync between local storage and the parent class props
22
21
  const model = {
23
22
  value: '',
24
23
  inactiveAt: 0,
25
24
  expiresAt: 0,
26
25
  updatedAt: Date.now(),
27
- sessionReplayMode: MODE.OFF,
26
+ sessionReplayMode: null,
28
27
  sessionReplaySentFirstChunk: false,
29
- sessionTraceMode: MODE.OFF,
28
+ sessionTraceMode: null,
30
29
  traceHarvestStarted: false,
31
- loggingMode: LOGGING_MODE.OFF,
32
- logApiMode: LOGGING_MODE.OFF,
30
+ loggingMode: null,
31
+ logApiMode: null,
33
32
  serverTimeDiff: null,
34
33
  // set by TimeKeeper; "undefined" value will not be stringified and stored but "null" will
35
34
  custom: {},
36
35
  numOfResets: 0,
37
- consent: false // set by consent() API call
36
+ consent: false,
37
+ // set by consent() API call
38
+ cachedRumResponse: null
38
39
  };
39
40
  export class SessionEntity {
40
41
  /**
@@ -246,7 +247,6 @@ export class SessionEntity {
246
247
  this.expiresTimer?.clear?.();
247
248
  delete this.isNew;
248
249
  this.setup({
249
- agentRef: this.agentRef,
250
250
  key: this.key,
251
251
  storage: this.storage,
252
252
  expiresMs: this.expiresMs,
@@ -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
 
@@ -12,7 +12,7 @@ import { stringify } from './stringify';
12
12
  * @returns {(number|undefined)} - The size of the data or undefined if size cannot be determined.
13
13
  */
14
14
  export function dataSize(data) {
15
- if (typeof data === 'string' && data.length) return data.length;
15
+ if (typeof data === 'string') return data.length;
16
16
  if (typeof data !== 'object') return undefined;
17
17
  // eslint-disable-next-line
18
18
  if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer && data.byteLength) return data.byteLength;
@@ -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 { isFileProtocol } from '../url/protocol';
@@ -10,6 +10,7 @@ import { warn } from './console';
10
10
  * @typedef {object} ObfuscationRule
11
11
  * @property {string|RegExp} regex The regular expression to match against in the payload
12
12
  * @property {string} [replacement] The string to replace the matched regex with
13
+ * @property {string[]} [eventFilter] An optional list of event types to which this rule should be applied. If not provided, or an empty array, the rule will be applied to all events.
13
14
  */
14
15
 
15
16
  /**
@@ -24,14 +25,33 @@ import { warn } from './console';
24
25
  */
25
26
 
26
27
  export class Obfuscator {
27
- constructor(agentRef) {
28
+ /**
29
+ * @param {Object} agentRef - Reference to the agent instance
30
+ * @param {string} [eventType] - Optional event type this obfuscator instance handles.
31
+ * If provided, only rules matching this event type (or rules with no eventFilter) will be applied.
32
+ */
33
+ constructor(agentRef, eventType) {
28
34
  this.agentRef = agentRef;
35
+ this.eventType = eventType;
29
36
  this.warnedRegexMissing = false;
30
37
  this.warnedInvalidRegex = false;
31
38
  this.warnedInvalidReplacement = false;
32
39
  }
33
40
  get obfuscateConfigRules() {
34
- return this.agentRef.init.obfuscate || [];
41
+ const allRules = this.agentRef.init.obfuscate || [];
42
+
43
+ // If this instance has no specific event type, return all rules
44
+ if (!this.eventType) return allRules;
45
+
46
+ // Filter rules to only those that apply to this instance's event type
47
+ return allRules.filter(rule => {
48
+ // If rule has no eventFilter, it applies to all events
49
+ if (!this.#hasValidEventFilter(rule)) {
50
+ return true;
51
+ }
52
+ // Otherwise, check if this instance's event type matches the rule's filter
53
+ return rule.eventFilter.includes(this.eventType);
54
+ });
35
55
  }
36
56
 
37
57
  /**
@@ -57,6 +77,130 @@ export class Obfuscator {
57
77
  }, input);
58
78
  }
59
79
 
80
+ /**
81
+ * Traverses an object and obfuscates all string properties.
82
+ * This instance will only apply rules that match its configured event type (if any).
83
+ * For features with mixed event types in their payloads (like generic_events), this will
84
+ * traverse the object and check each object's eventType property against the rules.
85
+ * @param {Object|Array} obj - The object or array to traverse
86
+ * @returns {Object|Array} The modified object
87
+ */
88
+ traverseAndObfuscateEvents(obj) {
89
+ if (!obj || typeof obj !== 'object') return obj;
90
+
91
+ // If this instance was configured with a specific event type, obfuscate everything
92
+ // (rules are already filtered in obfuscateConfigRules getter)
93
+ if (this.eventType) {
94
+ this.#applyFnToAllStrings(obj, this.obfuscateString.bind(this));
95
+ return obj;
96
+ }
97
+
98
+ // For generic obfuscators (no specific event types), check individual eventType properties
99
+ // This path is for features like generic_events that handle multiple event types
100
+ const eventTypesToObfuscate = new Set();
101
+ const globalRules = [];
102
+ const eventSpecificRules = [];
103
+ this.obfuscateConfigRules.forEach(rule => {
104
+ if (this.#hasValidEventFilter(rule)) {
105
+ eventSpecificRules.push(rule);
106
+ rule.eventFilter.forEach(eventType => eventTypesToObfuscate.add(eventType));
107
+ } else {
108
+ globalRules.push(rule);
109
+ }
110
+ });
111
+
112
+ // Optimization: if ALL rules are global (no event-specific rules exist),
113
+ // we can just apply all rules to everything without checking eventType properties
114
+ if (eventSpecificRules.length === 0) {
115
+ this.#applyFnToAllStrings(obj, this.obfuscateString.bind(this));
116
+ return obj;
117
+ }
118
+
119
+ // We have a mix of global and event-specific rules:
120
+ // - Apply event-specific rules only to matching eventTypes
121
+ // - Apply global rules to ALL event types
122
+ this.#applyFnWithEventTypeFilter(obj, this.obfuscateString.bind(this), Array.from(eventTypesToObfuscate), globalRules, null);
123
+ return obj;
124
+ }
125
+
126
+ /**
127
+ * Recursively applies a function to all string properties in an object.
128
+ * @param {Object|Array} obj - The object or array to traverse
129
+ * @param {Function} fn - The function to apply to string properties
130
+ * @private
131
+ */
132
+ #applyFnToAllStrings(obj, fn) {
133
+ if (!obj || typeof obj !== 'object') return;
134
+ Object.keys(obj).forEach(property => {
135
+ const value = obj[property];
136
+ if (typeof value === 'object' && value !== null) {
137
+ this.#applyFnToAllStrings(value, fn);
138
+ } else if (typeof value === 'string') {
139
+ obj[property] = fn(value);
140
+ }
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Recursively applies a function to string properties based on eventType filtering.
146
+ * @param {Object|Array} obj - The object or array to traverse
147
+ * @param {Function} fn - The function to apply to string properties (applies all rules)
148
+ * @param {string[]} eventTypes - Array of event types to apply event-specific obfuscation
149
+ * @param {Array} globalRules - Rules without eventFilter that apply to all events
150
+ * @param {boolean|null} shouldObfuscate - Track obfuscation state: null = not determined yet, true = obfuscate with all rules, false = obfuscate with global rules only
151
+ * @private
152
+ */
153
+ #applyFnWithEventTypeFilter(obj, fn, eventTypes, globalRules, shouldObfuscate) {
154
+ if (!obj || typeof obj !== 'object') return;
155
+
156
+ // Determine the obfuscation state for this object
157
+ let currentShouldObfuscate = shouldObfuscate;
158
+
159
+ // Check if this object has an eventType property
160
+ if ('eventType' in obj && typeof obj.eventType === 'string') {
161
+ currentShouldObfuscate = eventTypes.includes(obj.eventType);
162
+ }
163
+
164
+ // Process all properties
165
+ Object.keys(obj).forEach(property => {
166
+ const value = obj[property];
167
+ if (typeof value === 'object' && value !== null) {
168
+ // Recursively traverse objects/arrays, passing the current obfuscation state
169
+ this.#applyFnWithEventTypeFilter(value, fn, eventTypes, globalRules, currentShouldObfuscate);
170
+ } else if (typeof value === 'string') {
171
+ if (currentShouldObfuscate === true) {
172
+ // Apply all rules (both global and event-specific)
173
+ obj[property] = fn(value);
174
+ } else if (currentShouldObfuscate === false && globalRules.length > 0) {
175
+ // Apply only global rules
176
+ obj[property] = this.#applyRulesToString(value, globalRules);
177
+ }
178
+ // If currentShouldObfuscate is null, we haven't found an eventType yet, so don't obfuscate
179
+ }
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Applies specific obfuscation rules to a string.
185
+ * @param {string} input - The string to obfuscate
186
+ * @param {Array} rules - The rules to apply
187
+ * @returns {string} The obfuscated string
188
+ * @private
189
+ */
190
+ #applyRulesToString(input, rules) {
191
+ if (typeof input !== 'string' || input.trim().length === 0) return input;
192
+ const validatedRules = rules.map(rule => this.validateObfuscationRule(rule));
193
+ return validatedRules.filter(ruleValidation => ruleValidation.isValid).reduce((input, ruleValidation) => {
194
+ const {
195
+ rule
196
+ } = ruleValidation;
197
+ return input.replace(rule.regex, rule.replacement || '*');
198
+ }, input);
199
+ }
200
+ #hasValidEventFilter(rule) {
201
+ return Array.isArray(rule?.eventFilter) && rule.eventFilter.length > 0;
202
+ }
203
+
60
204
  /**
61
205
  * Validates an obfuscation rule and provides errors if any are found.
62
206
  * @param {ObfuscationRule} rule The rule to validate