@newrelic/browser-agent 1.236.0 → 1.237.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 (118) hide show
  1. package/dist/cjs/common/config/state/init.js +1 -0
  2. package/dist/cjs/common/config/state/runtime.js +2 -1
  3. package/dist/cjs/common/constants/env.cdn.js +1 -1
  4. package/dist/cjs/common/constants/env.npm.js +1 -1
  5. package/dist/cjs/common/deny-list/deny-list.js +14 -10
  6. package/dist/cjs/common/harvest/harvest.js +8 -22
  7. package/dist/cjs/common/util/submit-data.js +4 -36
  8. package/dist/cjs/common/wrap/wrap-jsonp.js +12 -6
  9. package/dist/cjs/features/ajax/aggregate/index.js +24 -27
  10. package/dist/cjs/features/jserrors/aggregate/compute-stack-trace.js +1 -1
  11. package/dist/cjs/features/jserrors/constants.js +2 -4
  12. package/dist/cjs/features/jserrors/instrument/index.js +79 -88
  13. package/dist/cjs/features/jserrors/instrument/uncaught-error.js +22 -0
  14. package/dist/cjs/features/metrics/aggregate/index.js +8 -0
  15. package/dist/cjs/features/page_view_event/aggregate/initialized-features.js +23 -19
  16. package/dist/cjs/features/session_replay/aggregate/index.js +65 -34
  17. package/dist/cjs/features/session_trace/aggregate/index.js +3 -4
  18. package/dist/cjs/features/spa/aggregate/index.js +1 -1
  19. package/dist/cjs/features/utils/instrument-base.js +6 -8
  20. package/dist/cjs/loaders/agent-base.js +87 -0
  21. package/dist/cjs/loaders/agent.js +41 -1
  22. package/dist/cjs/loaders/api/api.js +1 -1
  23. package/dist/cjs/loaders/api/interaction-types.js +87 -0
  24. package/dist/cjs/loaders/configure/configure.js +3 -1
  25. package/dist/cjs/loaders/micro-agent.js +3 -1
  26. package/dist/esm/common/config/state/init.js +1 -0
  27. package/dist/esm/common/config/state/runtime.js +2 -1
  28. package/dist/esm/common/constants/env.cdn.js +1 -1
  29. package/dist/esm/common/constants/env.npm.js +1 -1
  30. package/dist/esm/common/deny-list/deny-list.js +14 -10
  31. package/dist/esm/common/harvest/harvest.js +7 -22
  32. package/dist/esm/common/util/submit-data.js +4 -35
  33. package/dist/esm/common/wrap/wrap-jsonp.js +12 -6
  34. package/dist/esm/features/ajax/aggregate/index.js +25 -28
  35. package/dist/esm/features/jserrors/aggregate/compute-stack-trace.js +1 -1
  36. package/dist/esm/features/jserrors/constants.js +1 -2
  37. package/dist/esm/features/jserrors/instrument/index.js +78 -87
  38. package/dist/esm/features/jserrors/instrument/uncaught-error.js +15 -0
  39. package/dist/esm/features/metrics/aggregate/index.js +8 -0
  40. package/dist/esm/features/page_view_event/aggregate/initialized-features.js +23 -19
  41. package/dist/esm/features/session_replay/aggregate/index.js +65 -34
  42. package/dist/esm/features/session_trace/aggregate/index.js +3 -4
  43. package/dist/esm/features/spa/aggregate/index.js +1 -1
  44. package/dist/esm/features/utils/instrument-base.js +7 -9
  45. package/dist/esm/loaders/agent-base.js +80 -0
  46. package/dist/esm/loaders/agent.js +41 -1
  47. package/dist/esm/loaders/api/api.js +1 -1
  48. package/dist/esm/loaders/api/interaction-types.js +80 -0
  49. package/dist/esm/loaders/configure/configure.js +5 -3
  50. package/dist/esm/loaders/micro-agent.js +3 -1
  51. package/dist/types/common/config/state/runtime.d.ts.map +1 -1
  52. package/dist/types/common/event-emitter/register-handler.d.ts +1 -1
  53. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  54. package/dist/types/common/session/session-entity.d.ts +6 -6
  55. package/dist/types/common/util/submit-data.d.ts +2 -20
  56. package/dist/types/common/util/submit-data.d.ts.map +1 -1
  57. package/dist/types/common/window/nreum.d.ts +2 -2
  58. package/dist/types/common/wrap/wrap-jsonp.d.ts.map +1 -1
  59. package/dist/types/features/ajax/aggregate/index.d.ts +5 -5
  60. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  61. package/dist/types/features/jserrors/constants.d.ts +0 -1
  62. package/dist/types/features/jserrors/constants.d.ts.map +1 -1
  63. package/dist/types/features/jserrors/instrument/index.d.ts +0 -13
  64. package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
  65. package/dist/types/features/jserrors/instrument/uncaught-error.d.ts +15 -0
  66. package/dist/types/features/jserrors/instrument/uncaught-error.d.ts.map +1 -0
  67. package/dist/types/features/metrics/aggregate/endpoint-map.d.ts +5 -5
  68. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  69. package/dist/types/features/page_view_event/aggregate/initialized-features.d.ts.map +1 -1
  70. package/dist/types/features/session_replay/aggregate/index.d.ts +16 -30
  71. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  72. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  73. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  74. package/dist/types/loaders/agent-base.d.ts +59 -0
  75. package/dist/types/loaders/agent-base.d.ts.map +1 -0
  76. package/dist/types/loaders/agent.d.ts +35 -1
  77. package/dist/types/loaders/agent.d.ts.map +1 -1
  78. package/dist/types/loaders/api/interaction-types.d.ts +122 -0
  79. package/dist/types/loaders/api/interaction-types.d.ts.map +1 -0
  80. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  81. package/dist/types/loaders/features/features.d.ts +9 -9
  82. package/dist/types/loaders/micro-agent.d.ts +3 -2
  83. package/dist/types/loaders/micro-agent.d.ts.map +1 -1
  84. package/package.json +1 -1
  85. package/src/common/config/state/init.js +1 -1
  86. package/src/common/config/state/runtime.js +2 -1
  87. package/src/common/deny-list/deny-list.js +11 -11
  88. package/src/common/deny-list/deny-list.test.js +31 -0
  89. package/src/common/harvest/harvest.js +8 -18
  90. package/src/common/harvest/harvest.test.js +16 -36
  91. package/src/common/util/__mocks__/submit-data.js +0 -1
  92. package/src/common/util/submit-data.js +2 -24
  93. package/src/common/util/submit-data.test.js +0 -56
  94. package/src/common/wrap/wrap-jsonp.js +11 -6
  95. package/src/features/ajax/aggregate/index.js +25 -31
  96. package/src/features/jserrors/aggregate/compute-stack-trace.js +1 -1
  97. package/src/features/jserrors/constants.js +0 -1
  98. package/src/features/jserrors/instrument/index.js +91 -87
  99. package/src/features/jserrors/instrument/uncaught-error.js +15 -0
  100. package/src/features/metrics/aggregate/index.js +8 -0
  101. package/src/features/page_view_event/aggregate/initialized-features.js +18 -14
  102. package/src/features/session_replay/aggregate/index.component-test.js +17 -56
  103. package/src/features/session_replay/aggregate/index.js +47 -28
  104. package/src/features/session_trace/aggregate/index.js +3 -4
  105. package/src/features/spa/aggregate/index.js +1 -1
  106. package/src/features/utils/instrument-base.js +6 -9
  107. package/src/features/utils/instrument-base.test.js +7 -0
  108. package/src/loaders/agent-base.js +81 -0
  109. package/src/loaders/agent.js +42 -1
  110. package/src/loaders/api/api.js +1 -1
  111. package/src/loaders/api/interaction-types.js +80 -0
  112. package/src/loaders/configure/configure.js +14 -4
  113. package/src/loaders/micro-agent.js +4 -1
  114. package/dist/cjs/features/jserrors/instrument/debug.js +0 -40
  115. package/dist/esm/features/jserrors/instrument/debug.js +0 -38
  116. package/dist/types/features/jserrors/instrument/debug.d.ts +0 -2
  117. package/dist/types/features/jserrors/instrument/debug.d.ts.map +0 -1
  118. package/src/features/jserrors/instrument/debug.js +0 -36
@@ -23,7 +23,8 @@ const model = {
23
23
  releaseIds: {},
24
24
  session: undefined,
25
25
  xhrWrappable: typeof globalScope.XMLHttpRequest?.prototype?.addEventListener === 'function',
26
- version: VERSION
26
+ version: VERSION,
27
+ denyList: undefined
27
28
  };
28
29
  const _cache = {};
29
30
  export function getRuntime(id) {
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.236.0";
9
+ export const VERSION = "1.237.1";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.236.0";
9
+ export const VERSION = "1.237.1";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -40,24 +40,28 @@ export function setDenyList(denyListConfig) {
40
40
  return;
41
41
  }
42
42
  for (var i = 0; i < denyListConfig.length; i++) {
43
- var url = denyListConfig[i];
43
+ let url = denyListConfig[i];
44
+ if (!url) continue; // ignore bad values like undefined or empty strings
45
+
44
46
  if (url.indexOf('http://') === 0) {
45
47
  url = url.substring(7);
46
48
  } else if (url.indexOf('https://') === 0) {
47
49
  url = url.substring(8);
48
50
  }
49
- var firstSlash = url.indexOf('/');
51
+ const firstSlash = url.indexOf('/');
52
+ let host, pathname;
50
53
  if (firstSlash > 0) {
51
- denyList.push({
52
- hostname: url.substring(0, firstSlash),
53
- pathname: url.substring(firstSlash)
54
- });
54
+ host = url.substring(0, firstSlash);
55
+ pathname = url.substring(firstSlash);
55
56
  } else {
56
- denyList.push({
57
- hostname: url,
58
- pathname: ''
59
- });
57
+ host = url;
58
+ pathname = '';
60
59
  }
60
+ let [hostname, port] = host.split(':');
61
+ denyList.push({
62
+ hostname,
63
+ pathname
64
+ });
61
65
  }
62
66
  }
63
67
  /**
@@ -16,6 +16,8 @@ import { applyFnToProps } from '../util/traverse';
16
16
  import { SharedContext } from '../context/shared-context';
17
17
  import { VERSION } from "../constants/env.npm";
18
18
  import { isWorkerScope, isIE } from '../constants/runtime';
19
+ import { warn } from '../util/console';
20
+ const warnings = {};
19
21
 
20
22
  /**
21
23
  * @typedef {import('./types.js').NetworkSendSpec} NetworkSendSpec
@@ -24,7 +26,6 @@ import { isWorkerScope, isIE } from '../constants/runtime';
24
26
  * @typedef {import('./types.js').FeatureHarvestCallback} FeatureHarvestCallback
25
27
  * @typedef {import('./types.js').FeatureHarvestCallbackOptions} FeatureHarvestCallbackOptions
26
28
  */
27
-
28
29
  export class Harvest extends SharedContext {
29
30
  constructor(parent) {
30
31
  super(parent); // gets any allowed properties from the parent and stores them in `sharedContext`
@@ -135,13 +136,15 @@ export class Harvest extends SharedContext {
135
136
  payloadParams = payloadParams.substring(1);
136
137
  }
137
138
  const fullUrl = "".concat(url, "?").concat(baseParams).concat(payloadParams);
138
- const gzip = qs.content_encoding === 'gzip';
139
+ const gzip = !!qs?.attributes?.includes('gzip');
139
140
  if (!gzip) {
140
141
  if (endpoint === 'events') {
141
142
  body = body.e;
142
143
  } else {
143
144
  body = stringify(body);
144
145
  }
146
+ /** Warn --once per endpoint-- if the agent tries to send large payloads */
147
+ if (body.length > 750000 && (warnings[endpoint] = (warnings?.[endpoint] || 0) + 1) === 1) warn("The Browser Agent is attempting to send a very large payload to /".concat(endpoint, ". This is usually tied to large amounts of custom attributes. Please check your configurations."));
145
148
  }
146
149
  if (!body || body.length === 0 || body === '{}' || body === '[]') {
147
150
  // If body is null, undefined, or an empty object or array, send an empty string instead
@@ -188,23 +191,6 @@ export class Harvest extends SharedContext {
188
191
  cbFinished(cbResult);
189
192
  }, eventListenerOpts(false));
190
193
  }
191
-
192
- // if beacon request failed, retry with an alternative method -- will not happen for workers
193
- if (!result && submitMethod === submitData.beacon) {
194
- // browsers that support sendBeacon also support fetch with keepalive - IE will not retry unload calls
195
- submitMethod = submitData.fetchKeepAlive;
196
- try {
197
- submitMethod({
198
- url: fullUrl,
199
- body,
200
- headers
201
- });
202
- } catch (e) {
203
- // Ignore error in final harvest
204
- } finally {
205
- result = true;
206
- }
207
- }
208
194
  return result;
209
195
  }
210
196
 
@@ -264,9 +250,8 @@ export class Harvest extends SharedContext {
264
250
  cleanPayload() {
265
251
  let payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
266
252
  const clean = input => {
267
- if (typeof Uint8Array !== 'undefined' && input instanceof Uint8Array) {
268
- return input.length > 0 ? input : null;
269
- }
253
+ if (typeof Uint8Array !== 'undefined' && input instanceof Uint8Array || Array.isArray(input)) return input;
254
+ if (typeof input === 'string') return input.length > 0 ? input : null;
270
255
  return Object.entries(input || {}).reduce((accumulator, _ref2) => {
271
256
  let [key, value] = _ref2;
272
257
  if (typeof value === 'number' || typeof value === 'string' && value.length > 0 || typeof value === 'object' && Object.keys(value || {}).length > 0) {
@@ -7,7 +7,7 @@
7
7
  import { isBrowserScope, supportsSendBeacon } from '../constants/runtime';
8
8
 
9
9
  /**
10
- * @typedef {xhr|fetchKeepAlive|beacon} NetworkMethods
10
+ * @typedef {xhr|beacon} NetworkMethods
11
11
  */
12
12
 
13
13
  /**
@@ -64,36 +64,6 @@ export function xhr(_ref) {
64
64
  return request;
65
65
  }
66
66
 
67
- /**
68
- * Send via fetch with keepalive true
69
- * @param {Object} args - The args.
70
- * @param {string} args.url - The URL to send to.
71
- * @param {string=} args.body - The Stringified body.
72
- * @param {string=} [args.method=POST] - The XHR method to use.
73
- * @param {{key: string, value: string}[]} [args.headers] - The headers to attach.
74
- * @returns {XMLHttpRequest}
75
- */
76
- export function fetchKeepAlive(_ref2) {
77
- let {
78
- url,
79
- body = null,
80
- method = 'POST',
81
- headers = [{
82
- key: 'content-type',
83
- value: 'text/plain'
84
- }]
85
- } = _ref2;
86
- return fetch(url, {
87
- method,
88
- headers: headers.reduce((aggregator, header) => {
89
- aggregator.push([header.key, header.value]);
90
- return aggregator;
91
- }, []),
92
- body,
93
- keepalive: true
94
- });
95
- }
96
-
97
67
  /**
98
68
  * Send via sendBeacon. Do NOT call this function outside of a guaranteed web window environment.
99
69
  * @param {Object} args - The args
@@ -101,11 +71,11 @@ export function fetchKeepAlive(_ref2) {
101
71
  * @param {string=} args.body - The Stringified body
102
72
  * @returns {boolean}
103
73
  */
104
- export function beacon(_ref3) {
74
+ export function beacon(_ref2) {
105
75
  let {
106
76
  url,
107
77
  body
108
- } = _ref3;
78
+ } = _ref2;
109
79
  try {
110
80
  // Navigator has to be bound to ensure it does not error in some browsers
111
81
  // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
@@ -113,8 +83,7 @@ export function beacon(_ref3) {
113
83
  return send(url, body);
114
84
  } catch (err) {
115
85
  // if sendBeacon still trys to throw an illegal invocation error,
116
- // we can catch here and return. The harvest module will fallback to use
117
- // fetchKeepAlive to try to send
86
+ // we can catch here and return
118
87
  return false;
119
88
  }
120
89
  }
@@ -86,13 +86,19 @@ export function wrapJsonP(sharedEE) {
86
86
  var matches = src.match(CALLBACK_REGEX);
87
87
  return matches ? matches[1] : null;
88
88
  }
89
+
90
+ /**
91
+ * Traverse a nested object using a '.'-delimited string wherein each substring piece maps to each subsequent object property layer.
92
+ * @param {string} longKey
93
+ * @param {object} obj
94
+ * @returns The final nested object referred to by initial longKey.
95
+ */
89
96
  function discoverValue(longKey, obj) {
90
- var matches = longKey.match(VALUE_REGEX);
91
- var key = matches[1];
92
- var remaining = matches[3];
93
- if (!remaining) {
94
- return obj[key];
95
- }
97
+ if (!longKey) return obj; // end of object recursion depth when no more key levels
98
+ const matches = longKey.match(VALUE_REGEX);
99
+ // if 'longKey' was not undefined, that is it at least had 1 level left, then the regexp would've at least matched 1st group
100
+ const key = matches[1];
101
+ const remaining = matches[3];
96
102
  return discoverValue(remaining, obj[key]);
97
103
  }
98
104
  function discoverParent(key) {
@@ -6,7 +6,7 @@ import { registerHandler as register } from '../../../common/event-emitter/regis
6
6
  import { stringify } from '../../../common/util/stringify';
7
7
  import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer';
8
8
  import { handle } from '../../../common/event-emitter/handle';
9
- import { getConfigurationValue, getInfo } from '../../../common/config/config';
9
+ import { getConfiguration, getInfo, getRuntime } from '../../../common/config/config';
10
10
  import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler';
11
11
  import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list';
12
12
  import { FEATURE_NAME } from '../constants';
@@ -18,13 +18,22 @@ export class Aggregate extends AggregateBase {
18
18
  static featureName = FEATURE_NAME;
19
19
  constructor(agentIdentifier, aggregator) {
20
20
  super(agentIdentifier, aggregator, FEATURE_NAME);
21
+ const agentInit = getConfiguration(agentIdentifier);
22
+ const allAjaxIsEnabled = agentInit.ajax.enabled !== false;
23
+ register('xhr', storeXhr, this.featureName, this.ee);
24
+ if (!allAjaxIsEnabled) {
25
+ drain(this.agentIdentifier, this.featureName);
26
+ return; // feature will only collect timeslice metrics & ajax trace nodes if it's not fully enabled
27
+ }
28
+
29
+ const denyList = getRuntime(agentIdentifier).denyList;
30
+ setDenyList(denyList);
21
31
  let ajaxEvents = [];
22
32
  let spaAjaxEvents = {};
23
33
  let sentAjaxEvents = [];
24
- let scheduler;
25
34
  const ee = this.ee;
26
- const harvestTimeSeconds = getConfigurationValue(agentIdentifier, 'ajax.harvestTimeSeconds') || 10;
27
- const MAX_PAYLOAD_SIZE = getConfigurationValue(agentIdentifier, 'ajax.maxPayloadSize') || 1000000;
35
+ const harvestTimeSeconds = agentInit.ajax.harvestTimeSeconds || 10;
36
+ const MAX_PAYLOAD_SIZE = agentInit.ajax.maxPayloadSize || 1000000;
28
37
 
29
38
  // Exposes these methods to browser test files -- future TO DO: can be removed once these fns are extracted from the constructor into class func
30
39
  this.storeXhr = storeXhr;
@@ -41,24 +50,22 @@ export class Aggregate extends AggregateBase {
41
50
  delete spaAjaxEvents[interaction.id];
42
51
  });
43
52
  ee.on('interactionDiscarded', interaction => {
44
- if (!spaAjaxEvents[interaction.id] || !allAjaxIsEnabled()) return;
53
+ if (!spaAjaxEvents[interaction.id]) return;
45
54
  spaAjaxEvents[interaction.id].forEach(function (item) {
46
55
  // move it from the spaAjaxEvents buffer to the ajaxEvents buffer for harvesting here
47
56
  ajaxEvents.push(item);
48
57
  });
49
58
  delete spaAjaxEvents[interaction.id];
50
59
  });
51
- if (allAjaxIsEnabled()) setDenyList(getConfigurationValue(agentIdentifier, 'ajax.deny_list'));
52
- register('xhr', storeXhr, this.featureName, this.ee);
53
- if (allAjaxIsEnabled()) {
54
- scheduler = new HarvestScheduler('events', {
55
- onFinished: onEventsHarvestFinished,
56
- getPayload: prepareHarvest
57
- }, this);
58
- ee.on("drain-".concat(this.featureName), () => {
59
- scheduler.startTimer(harvestTimeSeconds);
60
- });
61
- }
60
+ const scheduler = new HarvestScheduler('events', {
61
+ onFinished: onEventsHarvestFinished,
62
+ getPayload: prepareHarvest
63
+ }, this);
64
+ ee.on("drain-".concat(this.featureName), () => {
65
+ scheduler.startTimer(harvestTimeSeconds);
66
+ });
67
+ drain(this.agentIdentifier, this.featureName);
68
+ return;
62
69
  function storeXhr(params, metrics, startTime, endTime, type) {
63
70
  metrics.time = startTime;
64
71
 
@@ -73,9 +80,7 @@ export class Aggregate extends AggregateBase {
73
80
 
74
81
  // store as metric
75
82
  aggregator.store('xhr', hash, params, metrics);
76
- if (!allAjaxIsEnabled()) {
77
- return;
78
- }
83
+ if (!allAjaxIsEnabled) return;
79
84
  if (!shouldCollectEvent(params)) {
80
85
  if (params.hostname === getInfo(agentIdentifier).errorBeacon) {
81
86
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Excluded/Agent'], undefined, FEATURE_NAMES.metrics, ee);
@@ -157,7 +162,7 @@ export class Aggregate extends AggregateBase {
157
162
  return tooBig ? getPayload(events, maxPayloadSize, ++chunks) : payload;
158
163
  }
159
164
  function onEventsHarvestFinished(result) {
160
- if (result.retry && sentAjaxEvents.length > 0 && allAjaxIsEnabled()) {
165
+ if (result.retry && sentAjaxEvents.length > 0) {
161
166
  ajaxEvents.unshift(...sentAjaxEvents);
162
167
  sentAjaxEvents = [];
163
168
  }
@@ -206,13 +211,5 @@ export class Aggregate extends AggregateBase {
206
211
  return this.payload.length * 2 > maxPayloadSize;
207
212
  };
208
213
  }
209
- function allAjaxIsEnabled() {
210
- var enabled = getConfigurationValue(agentIdentifier, 'ajax.enabled');
211
- if (enabled === false) {
212
- return false;
213
- }
214
- return true;
215
- }
216
- drain(this.agentIdentifier, this.featureName);
217
214
  }
218
215
  }
@@ -222,7 +222,7 @@ function computeStackTraceBySourceAndLine(ex) {
222
222
  mode: 'sourceline',
223
223
  name: className,
224
224
  message: ex.message,
225
- stackString: getClassName(ex) + ': ' + ex.message + '\n in evaluated code',
225
+ stackString: className + ': ' + ex.message + '\n in evaluated code',
226
226
  frames: [{
227
227
  func: 'evaluated code'
228
228
  }]
@@ -1,3 +1,2 @@
1
1
  import { FEATURE_NAMES } from '../../loaders/features/features';
2
- export const FEATURE_NAME = FEATURE_NAMES.jserrors;
3
- export const NR_ERR_PROP = 'nr@seenError';
2
+ export const FEATURE_NAME = FEATURE_NAMES.jserrors;
@@ -5,63 +5,53 @@
5
5
 
6
6
  import { handle } from '../../../common/event-emitter/handle';
7
7
  import { now } from '../../../common/timing/now';
8
- import { getOrSet } from '../../../common/util/get-or-set';
9
- import { wrapRaf, wrapTimer, wrapEvents, wrapXhr } from '../../../common/wrap';
10
- import './debug';
11
8
  import { InstrumentBase } from '../../utils/instrument-base';
12
- import { FEATURE_NAME, NR_ERR_PROP } from '../constants';
9
+ import { FEATURE_NAME } from '../constants';
13
10
  import { FEATURE_NAMES } from '../../../loaders/features/features';
14
11
  import { globalScope } from '../../../common/constants/runtime';
15
12
  import { eventListenerOpts } from '../../../common/event-listener/event-listener-opts';
16
- import { getRuntime } from '../../../common/config/config';
17
13
  import { stringify } from '../../../common/util/stringify';
14
+ import { UncaughtError } from './uncaught-error';
18
15
  export class Instrument extends InstrumentBase {
19
16
  static featureName = FEATURE_NAME;
17
+ #seenErrors = new Set();
20
18
  constructor(agentIdentifier, aggregator) {
21
19
  let auto = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
22
20
  super(agentIdentifier, aggregator, FEATURE_NAME, auto);
23
- // skipNext counter to keep track of uncaught
24
- // errors that will be the same as caught errors.
25
- this.skipNext = 0;
26
21
  try {
27
22
  // this try-catch can be removed when IE11 is completely unsupported & gone
28
23
  this.removeOnAbort = new AbortController();
29
24
  } catch (e) {}
30
- const thisInstrument = this;
31
- thisInstrument.ee.on('fn-start', function (args, obj, methodName) {
32
- if (thisInstrument.abortHandler) thisInstrument.skipNext += 1;
33
- });
34
- thisInstrument.ee.on('fn-err', function (args, obj, err) {
35
- if (thisInstrument.abortHandler && !err[NR_ERR_PROP]) {
36
- getOrSet(err, NR_ERR_PROP, function getVal() {
37
- return true;
38
- });
39
- this.thrown = true;
40
- handle('err', [err, now()], undefined, FEATURE_NAMES.jserrors, thisInstrument.ee);
41
- }
42
- });
43
- thisInstrument.ee.on('fn-end', function () {
44
- if (!thisInstrument.abortHandler) return;
45
- if (!this.thrown && thisInstrument.skipNext > 0) thisInstrument.skipNext -= 1;
25
+
26
+ // Capture function errors early in case the spa feature is loaded
27
+ this.ee.on('fn-err', (args, obj, error) => {
28
+ if (!this.abortHandler || this.#seenErrors.has(error)) return;
29
+ this.#seenErrors.add(error);
30
+ handle('err', [this.#castError(error), now()], undefined, FEATURE_NAMES.jserrors, this.ee);
46
31
  });
47
- thisInstrument.ee.on('internal-error', function (e) {
48
- handle('ierr', [e, now(), true], undefined, FEATURE_NAMES.jserrors, thisInstrument.ee);
32
+ this.ee.on('internal-error', error => {
33
+ if (!this.abortHandler) return;
34
+ handle('ierr', [this.#castError(error), now(), true], undefined, FEATURE_NAMES.jserrors, this.ee);
49
35
  });
50
-
51
- // Replace global error handler with our own.
52
- this.origOnerror = globalScope.onerror;
53
- globalScope.onerror = this.onerrorHandler.bind(this);
54
- globalScope.addEventListener('unhandledrejection', e => {
55
- /** rejections can contain data of any type -- this is an effort to keep the message human readable */
56
- const err = castReasonToError(e.reason);
57
- handle('err', [err, now(), false, {
36
+ globalScope.addEventListener('unhandledrejection', promiseRejectionEvent => {
37
+ if (!this.abortHandler) return;
38
+ handle('err', [this.#castPromiseRejectionEvent(promiseRejectionEvent), now(), false, {
58
39
  unhandledPromiseRejection: 1
59
40
  }], undefined, FEATURE_NAMES.jserrors, this.ee);
60
41
  }, eventListenerOpts(false, this.removeOnAbort?.signal));
61
- wrapRaf(this.ee);
62
- wrapTimer(this.ee);
63
- wrapEvents(this.ee);
64
- if (getRuntime(agentIdentifier).xhrWrappable) wrapXhr(this.ee);
42
+ globalScope.addEventListener('error', errorEvent => {
43
+ if (!this.abortHandler) return;
44
+
45
+ /**
46
+ * If the spa feature is loaded, errors may already have been captured in the `fn-err` listener above.
47
+ * This ensures those errors are not captured twice.
48
+ */
49
+ if (this.#seenErrors.has(errorEvent.error)) {
50
+ this.#seenErrors.delete(errorEvent.error);
51
+ return;
52
+ }
53
+ handle('err', [this.#castErrorEvent(errorEvent), now()], undefined, FEATURE_NAMES.jserrors, this.ee);
54
+ }, eventListenerOpts(false, this.removeOnAbort?.signal));
65
55
  this.abortHandler = this.#abort; // we also use this as a flag to denote that the feature is active or on and handling errors
66
56
  this.importAggregator();
67
57
  }
@@ -69,65 +59,66 @@ export class Instrument extends InstrumentBase {
69
59
  /** Restoration and resource release tasks to be done if JS error loader is being aborted. Unwind changes to globals. */
70
60
  #abort() {
71
61
  this.removeOnAbort?.abort();
62
+ this.#seenErrors.clear();
72
63
  this.abortHandler = undefined; // weakly allow this abort op to run only once
73
64
  }
74
65
 
75
66
  /**
76
- * FF and Android browsers do not provide error info to the 'error' event callback,
77
- * so we must use window.onerror
78
- * @param {string} message
79
- * @param {string} filename
80
- * @param {number} lineno
81
- * @param {number} column
82
- * @param {Error | *} errorObj
83
- * @returns
67
+ * Any value can be used with the `throw` keyword. This function ensures that the value is
68
+ * either a proper Error instance or attempts to convert it to an UncaughtError instance.
69
+ * @param {any} error The value thrown
70
+ * @returns {Error|UncaughtError} The converted error instance
84
71
  */
85
- onerrorHandler(message, filename, lineno, column, errorObj) {
86
- if (typeof this.origOnerror === 'function') this.origOnerror(...arguments);
87
- try {
88
- if (this.skipNext) this.skipNext -= 1;else handle('err', [errorObj || new UncaughtException(message, filename, lineno), now()], undefined, FEATURE_NAMES.jserrors, this.ee);
89
- } catch (e) {
72
+ #castError(error) {
73
+ if (error instanceof Error) {
74
+ return error;
75
+ }
76
+
77
+ /**
78
+ * The thrown value may contain a message property. If it does, try to treat the thrown
79
+ * value as an Error-like object.
80
+ */
81
+ if (typeof error?.message !== 'undefined') {
82
+ return new UncaughtError(error.message, error.filename || error.sourceURL, error.lineno || error.line, error.colno || error.col);
83
+ }
84
+ return new UncaughtError(typeof error === 'string' ? error : stringify(error));
85
+ }
86
+
87
+ /**
88
+ * Attempts to convert a PromiseRejectionEvent object to an Error object
89
+ * @param {PromiseRejectionEvent} unhandledRejectionEvent The unhandled promise rejection event
90
+ * @returns {Error} An Error object with the message as the casted reason
91
+ */
92
+ #castPromiseRejectionEvent(promiseRejectionEvent) {
93
+ let prefix = 'Unhandled Promise Rejection: ';
94
+ if (promiseRejectionEvent?.reason instanceof Error) {
90
95
  try {
91
- handle('ierr', [e, now(), true], undefined, FEATURE_NAMES.jserrors, this.ee);
92
- } catch (err) {
93
- // do nothing
96
+ promiseRejectionEvent.reason.message = prefix + promiseRejectionEvent.reason.message;
97
+ return promiseRejectionEvent.reason;
98
+ } catch (e) {
99
+ return promiseRejectionEvent.reason;
94
100
  }
95
101
  }
96
- return false; // maintain default behavior of the error event of Window
102
+ if (typeof promiseRejectionEvent.reason === 'undefined') return new UncaughtError(prefix);
103
+ const error = this.#castError(promiseRejectionEvent.reason);
104
+ error.message = prefix + error.message;
105
+ return error;
97
106
  }
98
- }
99
-
100
- /**
101
- *
102
- * @param {string} message
103
- * @param {string} filename
104
- * @param {number} lineno
105
- */
106
- function UncaughtException(message, filename, lineno) {
107
- this.message = message || 'Uncaught error with no additional information';
108
- this.sourceURL = filename;
109
- this.line = lineno;
110
- }
111
107
 
112
- /**
113
- * Attempts to cast an unhandledPromiseRejection reason (reject(...)) to an Error object
114
- * @param {*} reason - The reason property from an unhandled promise rejection
115
- * @returns {Error} - An Error object with the message as the casted reason
116
- */
117
- function castReasonToError(reason) {
118
- let prefix = 'Unhandled Promise Rejection: ';
119
- if (reason instanceof Error) {
120
- try {
121
- reason.message = prefix + reason.message;
122
- return reason;
123
- } catch (e) {
124
- return reason;
108
+ /**
109
+ * Attempts to convert an ErrorEvent object to an Error object
110
+ * @param {ErrorEvent} errorEvent The error event
111
+ * @returns {Error|UncaughtError} The error event converted to an Error object
112
+ */
113
+ #castErrorEvent(errorEvent) {
114
+ if (errorEvent.error instanceof Error) {
115
+ return errorEvent.error;
125
116
  }
126
- }
127
- if (typeof reason === 'undefined') return new Error(prefix);
128
- try {
129
- return new Error(prefix + stringify(reason));
130
- } catch (err) {
131
- return new Error(prefix);
117
+
118
+ /**
119
+ * Older browsers do not contain the `error` property on the ErrorEvent instance.
120
+ * https://caniuse.com/mdn-api_errorevent_error
121
+ */
122
+ return new UncaughtError(errorEvent.message, errorEvent.filename, errorEvent.lineno, errorEvent.colno);
132
123
  }
133
124
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Represents an uncaught non Error type error. This class does
3
+ * not extend the Error class to prevent an invalid stack trace
4
+ * from being created. Use this class to cast thrown errors that
5
+ * do not use the Error class (strings, etc) to an object.
6
+ */
7
+ export class UncaughtError {
8
+ constructor(message, filename, lineno, colno) {
9
+ this.name = 'UncaughtError';
10
+ this.message = message;
11
+ this.sourceURL = filename;
12
+ this.line = lineno;
13
+ this.column = colno;
14
+ }
15
+ }
@@ -148,6 +148,14 @@ export class Aggregate extends AggregateBase {
148
148
  // Capture metrics for size of custom attributes
149
149
  const jsAttributes = stringify(info.jsAttributes);
150
150
  this.storeSupportabilityMetrics('PageSession/Feature/CustomData/Bytes', jsAttributes === '{}' ? 0 : jsAttributes.length);
151
+
152
+ // Capture metrics for performance markers and measures
153
+ if (typeof performance !== 'undefined') {
154
+ const markers = performance.getEntriesByType('mark');
155
+ const measures = performance.getEntriesByType('measure');
156
+ this.storeSupportabilityMetrics('Generic/Performance/Mark/Seen', markers.length);
157
+ this.storeSupportabilityMetrics('Generic/Performance/Measure/Seen', measures.length);
158
+ }
151
159
  } catch (e) {
152
160
  // do nothing
153
161
  }
@@ -1,4 +1,5 @@
1
1
  import { FEATURE_NAMES } from '../../../loaders/features/features';
2
+ import { gosNREUM } from '../../../common/window/nreum';
2
3
 
3
4
  /**
4
5
  * Get an array of flags required by downstream (NR UI) based on the features initialized in this agent
@@ -8,25 +9,28 @@ import { FEATURE_NAMES } from '../../../loaders/features/features';
8
9
  */
9
10
  export function getActivatedFeaturesFlags(agentId) {
10
11
  const flagArr = [];
11
- Object.keys(newrelic.initializedAgents[agentId].features).forEach(featName => {
12
- switch (featName) {
13
- case FEATURE_NAMES.ajax:
14
- flagArr.push('xhr');
15
- break;
16
- case FEATURE_NAMES.jserrors:
17
- flagArr.push('err');
18
- break;
19
- case FEATURE_NAMES.pageAction:
20
- flagArr.push('ins');
21
- break;
22
- case FEATURE_NAMES.sessionTrace:
23
- flagArr.push('stn');
24
- break;
25
- case FEATURE_NAMES.spa:
26
- flagArr.push('spa');
27
- break;
28
- }
29
- });
12
+ const newrelic = gosNREUM();
13
+ try {
14
+ Object.keys(newrelic.initializedAgents[agentId].features).forEach(featName => {
15
+ switch (featName) {
16
+ case FEATURE_NAMES.ajax:
17
+ flagArr.push('xhr');
18
+ break;
19
+ case FEATURE_NAMES.jserrors:
20
+ flagArr.push('err');
21
+ break;
22
+ case FEATURE_NAMES.pageAction:
23
+ flagArr.push('ins');
24
+ break;
25
+ case FEATURE_NAMES.sessionTrace:
26
+ flagArr.push('stn');
27
+ break;
28
+ case FEATURE_NAMES.spa:
29
+ flagArr.push('spa');
30
+ break;
31
+ }
32
+ });
33
+ } catch (e) {}
30
34
  return flagArr;
31
35
  }
32
36