@newrelic/browser-agent 1.275.0 → 1.277.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/cjs/common/config/init.js +44 -8
  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/features/ajax/instrument/index.js +1 -1
  6. package/dist/cjs/features/generic_events/aggregate/index.js +82 -8
  7. package/dist/cjs/features/generic_events/constants.js +8 -2
  8. package/dist/cjs/features/generic_events/instrument/index.js +20 -8
  9. package/dist/cjs/features/jserrors/aggregate/index.js +3 -2
  10. package/dist/cjs/features/jserrors/aggregate/internal-errors.js +2 -2
  11. package/dist/cjs/features/jserrors/instrument/index.js +2 -2
  12. package/dist/cjs/features/metrics/aggregate/index.js +1 -35
  13. package/dist/cjs/features/soft_navigations/aggregate/index.js +28 -11
  14. package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +2 -1
  15. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +12 -12
  16. package/dist/cjs/features/soft_navigations/constants.js +5 -2
  17. package/dist/cjs/loaders/api/api-methods.js +1 -1
  18. package/dist/cjs/loaders/api/api.js +2 -1
  19. package/dist/cjs/loaders/micro-agent-base.js +10 -0
  20. package/dist/esm/common/config/init.js +39 -3
  21. package/dist/esm/common/constants/env.cdn.js +1 -1
  22. package/dist/esm/common/constants/env.npm.js +1 -1
  23. package/dist/esm/features/ajax/instrument/index.js +1 -1
  24. package/dist/esm/features/generic_events/aggregate/index.js +84 -10
  25. package/dist/esm/features/generic_events/constants.js +7 -1
  26. package/dist/esm/features/generic_events/instrument/index.js +21 -9
  27. package/dist/esm/features/jserrors/aggregate/index.js +3 -2
  28. package/dist/esm/features/jserrors/aggregate/internal-errors.js +2 -2
  29. package/dist/esm/features/jserrors/instrument/index.js +2 -2
  30. package/dist/esm/features/metrics/aggregate/index.js +1 -35
  31. package/dist/esm/features/soft_navigations/aggregate/index.js +29 -12
  32. package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +2 -1
  33. package/dist/esm/features/soft_navigations/aggregate/interaction.js +13 -13
  34. package/dist/esm/features/soft_navigations/constants.js +4 -1
  35. package/dist/esm/loaders/api/api-methods.js +1 -1
  36. package/dist/esm/loaders/api/api.js +2 -1
  37. package/dist/esm/loaders/micro-agent-base.js +10 -0
  38. package/dist/types/common/config/init.d.ts.map +1 -1
  39. package/dist/types/features/generic_events/aggregate/index.d.ts +2 -0
  40. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  41. package/dist/types/features/generic_events/constants.d.ts +6 -0
  42. package/dist/types/features/generic_events/constants.d.ts.map +1 -1
  43. package/dist/types/features/generic_events/instrument/index.d.ts.map +1 -1
  44. package/dist/types/features/jserrors/aggregate/index.d.ts +2 -1
  45. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  46. package/dist/types/features/jserrors/aggregate/internal-errors.d.ts +1 -1
  47. package/dist/types/features/jserrors/aggregate/internal-errors.d.ts.map +1 -1
  48. package/dist/types/features/metrics/aggregate/index.d.ts +0 -1
  49. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  50. package/dist/types/features/soft_navigations/aggregate/index.d.ts +1 -0
  51. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  52. package/dist/types/features/soft_navigations/aggregate/initial-page-load-interaction.d.ts.map +1 -1
  53. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +3 -3
  54. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  55. package/dist/types/features/soft_navigations/constants.d.ts +1 -0
  56. package/dist/types/features/soft_navigations/constants.d.ts.map +1 -1
  57. package/dist/types/loaders/api/api.d.ts +1 -0
  58. package/dist/types/loaders/api/api.d.ts.map +1 -1
  59. package/dist/types/loaders/micro-agent-base.d.ts +7 -0
  60. package/dist/types/loaders/micro-agent-base.d.ts.map +1 -1
  61. package/package.json +13 -1
  62. package/src/common/config/init.js +21 -3
  63. package/src/features/ajax/instrument/index.js +1 -1
  64. package/src/features/generic_events/aggregate/index.js +88 -10
  65. package/src/features/generic_events/constants.js +8 -0
  66. package/src/features/generic_events/instrument/index.js +21 -10
  67. package/src/features/jserrors/aggregate/index.js +3 -2
  68. package/src/features/jserrors/aggregate/internal-errors.js +2 -2
  69. package/src/features/jserrors/instrument/index.js +2 -2
  70. package/src/features/metrics/aggregate/index.js +1 -33
  71. package/src/features/soft_navigations/aggregate/index.js +22 -11
  72. package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +2 -1
  73. package/src/features/soft_navigations/aggregate/interaction.js +13 -12
  74. package/src/features/soft_navigations/constants.js +3 -1
  75. package/src/loaders/api/api-methods.js +1 -1
  76. package/src/loaders/api/api.js +3 -1
  77. package/src/loaders/micro-agent-base.js +10 -0
@@ -18,8 +18,6 @@ var _belNode = require("./bel-node");
18
18
  class Interaction extends _belNode.BelNode {
19
19
  id = (0, _uniqueId.generateUuid)(); // unique id that is serialized and used to link interactions with errors
20
20
  initialPageURL = _runtime.initialLocation;
21
- oldURL = '' + _runtime.globalScope?.location;
22
- newURL = '' + _runtime.globalScope?.location;
23
21
  customName;
24
22
  customAttributes = {};
25
23
  customDataByApi = {};
@@ -34,7 +32,7 @@ class Interaction extends _belNode.BelNode {
34
32
  keepOpenUntilEndApi = false;
35
33
  onDone = [];
36
34
  cancellationTimer;
37
- constructor(agentIdentifier, uiEvent, uiEventTimestamp, currentRouteKnown) {
35
+ constructor(agentIdentifier, uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
38
36
  super(agentIdentifier);
39
37
  this.belType = _constants.NODE_TYPE.INTERACTION;
40
38
  this.trigger = uiEvent;
@@ -43,6 +41,7 @@ class Interaction extends _belNode.BelNode {
43
41
  this.eventSubscription = new Map([['finished', []], ['cancelled', []]]);
44
42
  this.forceSave = this.forceIgnore = false;
45
43
  if (this.trigger === _constants.API_TRIGGER_NAME) this.createdByApi = true;
44
+ this.newURL = this.oldURL = currentUrl || _runtime.globalScope?.location.href;
46
45
  }
47
46
  updateDom(timestamp) {
48
47
  this.domTimestamp = timestamp || (0, _now.now)(); // default timestamp should be precise for accurate isActiveDuring calculations
@@ -62,6 +61,8 @@ class Interaction extends _belNode.BelNode {
62
61
  done(customEndTime) {
63
62
  // User could've mark this interaction--regardless UI or api started--as "don't close until .end() is called on it". Only .end provides a timestamp; the default flows do not.
64
63
  if (this.keepOpenUntilEndApi && customEndTime === undefined) return false;
64
+ // If interaction is already closed, this is a no-op. However, returning true lets startUIInteraction know that it CAN start a new interaction, as this one is done.
65
+ if (this.status !== _constants.INTERACTION_STATUS.IP) return true;
65
66
  this.onDone.forEach(apiProvidedCb => apiProvidedCb(this.customDataByApi)); // this interaction's .save or .ignore can still be set by these user provided callbacks for example
66
67
 
67
68
  if (this.forceIgnore) this.#cancel(); // .ignore() always has precedence over save actions
@@ -71,7 +72,6 @@ class Interaction extends _belNode.BelNode {
71
72
  return true;
72
73
  }
73
74
  #finish(customEndTime = 0) {
74
- if (this.status !== _constants.INTERACTION_STATUS.IP) return; // disallow this call if the ixn is already done aka not in-progress
75
75
  clearTimeout(this.cancellationTimer);
76
76
  this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime);
77
77
  this.customAttributes = {
@@ -85,7 +85,6 @@ class Interaction extends _belNode.BelNode {
85
85
  callbacks.forEach(fn => fn());
86
86
  }
87
87
  #cancel() {
88
- if (this.status !== _constants.INTERACTION_STATUS.IP) return; // disallow this call if the ixn is already done aka not in-progress
89
88
  clearTimeout(this.cancellationTimer);
90
89
  this.status = _constants.INTERACTION_STATUS.CAN;
91
90
 
@@ -102,7 +101,7 @@ class Interaction extends _belNode.BelNode {
102
101
  */
103
102
  isActiveDuring(timestamp) {
104
103
  if (this.status === _constants.INTERACTION_STATUS.IP) return this.start <= timestamp;
105
- return this.status === _constants.INTERACTION_STATUS.FIN && this.start <= timestamp && this.end >= timestamp;
104
+ return this.status === _constants.INTERACTION_STATUS.FIN && this.start <= timestamp && this.end > timestamp;
106
105
  }
107
106
 
108
107
  // Following are virtual properties overridden by a subclass:
@@ -110,16 +109,17 @@ class Interaction extends _belNode.BelNode {
110
109
  get firstContentfulPaint() {}
111
110
  get navTiming() {}
112
111
  serialize(firstStartTimeOfPayload) {
112
+ const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined;
113
113
  const addString = (0, _belSerializer.getAddStringContext)(this.agentIdentifier);
114
114
  const nodeList = [];
115
115
  let ixnType;
116
- if (this.trigger === 'initialPageLoad') ixnType = _constants.INTERACTION_TYPE.INITIAL_PAGE_LOAD;else if (this.newURL !== this.oldURL) ixnType = _constants.INTERACTION_TYPE.ROUTE_CHANGE;else ixnType = _constants.INTERACTION_TYPE.UNSPECIFIED;
116
+ if (this.trigger === _constants.IPL_TRIGGER_NAME) ixnType = _constants.INTERACTION_TYPE.INITIAL_PAGE_LOAD;else if (this.newURL !== this.oldURL) ixnType = _constants.INTERACTION_TYPE.ROUTE_CHANGE;else ixnType = _constants.INTERACTION_TYPE.UNSPECIFIED;
117
117
 
118
118
  // IMPORTANT: The order in which addString is called matters and correlates to the order in which string shows up in the harvest payload. Do not re-order the following code.
119
119
  const fields = [(0, _belSerializer.numeric)(this.belType), 0,
120
120
  // this will be overwritten below with number of attached nodes
121
- (0, _belSerializer.numeric)(this.start - firstStartTimeOfPayload),
122
- // relative to first node
121
+ (0, _belSerializer.numeric)(this.start - (isFirstIxnOfPayload ? 0 : firstStartTimeOfPayload)),
122
+ // the very 1st ixn does not require offset so it should fallback to a 0 while rest is offset by the very 1st ixn's start
123
123
  (0, _belSerializer.numeric)(this.end - this.start),
124
124
  // end -- relative to start
125
125
  (0, _belSerializer.numeric)(this.callbackEnd),
@@ -130,9 +130,9 @@ class Interaction extends _belNode.BelNode {
130
130
  const allAttachedNodes = (0, _belSerializer.addCustomAttributes)(this.customAttributes || {}, addString); // start with all custom attributes
131
131
  if ((0, _info.getInfo)(this.agentIdentifier).atts) allAttachedNodes.push('a,' + addString((0, _info.getInfo)(this.agentIdentifier).atts)); // add apm provided attributes
132
132
  /* Querypack encoder+decoder quirkiness:
133
- - If first ixn node of payload is being processed, we use this node's start to offset. (firstStartTime should be 0--or undefined.)
134
- - Else for subsequent ixn nodes, we use the first ixn node's start to offset. */
135
- this.children.forEach(node => allAttachedNodes.push(node.serialize(firstStartTimeOfPayload || this.start))); // recursively add the serialized string of every child of this (ixn) bel node
133
+ - If first ixn node of payload is being processed, its children's start time must be offset by this node's start. (firstStartTime should be undefined.)
134
+ - Else for subsequent ixns in the same payload, we go back to using that first ixn node's start to offset their children's start. */
135
+ this.children.forEach(node => allAttachedNodes.push(node.serialize(isFirstIxnOfPayload ? this.start : firstStartTimeOfPayload))); // recursively add the serialized string of every child of this (ixn) bel node
136
136
 
137
137
  fields[1] = (0, _belSerializer.numeric)(allAttachedNodes.length);
138
138
  nodeList.push(fields);
@@ -3,15 +3,18 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.NODE_TYPE = exports.INTERACTION_TYPE = exports.INTERACTION_TRIGGERS = exports.INTERACTION_STATUS = exports.FEATURE_NAME = exports.API_TRIGGER_NAME = void 0;
6
+ exports.NODE_TYPE = exports.IPL_TRIGGER_NAME = exports.INTERACTION_TYPE = exports.INTERACTION_TRIGGERS = exports.INTERACTION_STATUS = exports.FEATURE_NAME = exports.API_TRIGGER_NAME = void 0;
7
7
  var _features = require("../../loaders/features/features");
8
8
  const INTERACTION_TRIGGERS = exports.INTERACTION_TRIGGERS = ['click',
9
9
  // e.g. user clicks link or the page back/forward buttons
10
10
  'keydown',
11
11
  // e.g. user presses left and right arrow key to switch between displayed photo gallery
12
- 'submit' // e.g. user clicks submit butotn or presses enter while editing a form field
12
+ 'submit',
13
+ // e.g. user clicks submit butotn or presses enter while editing a form field
14
+ 'popstate' // history api is used to navigate back and forward
13
15
  ];
14
16
  const API_TRIGGER_NAME = exports.API_TRIGGER_NAME = 'api';
17
+ const IPL_TRIGGER_NAME = exports.IPL_TRIGGER_NAME = 'initialPageLoad';
15
18
  const FEATURE_NAME = exports.FEATURE_NAME = _features.FEATURE_NAMES.softNav;
16
19
  const INTERACTION_TYPE = exports.INTERACTION_TYPE = {
17
20
  INITIAL_PAGE_LOAD: '',
@@ -5,5 +5,5 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.asyncApiMethods = exports.apiMethods = void 0;
7
7
  var _constants = require("../../features/session_replay/constants");
8
- const apiMethods = exports.apiMethods = ['setErrorHandler', 'finished', 'addToTrace', 'addRelease', 'addPageAction', 'setCurrentRouteName', 'setPageViewName', 'setCustomAttribute', 'interaction', 'noticeError', 'setUserId', 'setApplicationVersion', 'start', _constants.SR_EVENT_EMITTER_TYPES.RECORD, _constants.SR_EVENT_EMITTER_TYPES.PAUSE, 'log', 'wrapLogger'];
8
+ const apiMethods = exports.apiMethods = ['setErrorHandler', 'finished', 'addToTrace', 'addRelease', 'recordCustomEvent', 'addPageAction', 'setCurrentRouteName', 'setPageViewName', 'setCustomAttribute', 'interaction', 'noticeError', 'setUserId', 'setApplicationVersion', 'start', _constants.SR_EVENT_EMITTER_TYPES.RECORD, _constants.SR_EVENT_EMITTER_TYPES.PAUSE, 'log', 'wrapLogger'];
9
9
  const asyncApiMethods = exports.asyncApiMethods = ['setErrorHandler', 'finished', 'addToTrace', 'addRelease'];
@@ -80,6 +80,7 @@ function setAPI(agentIdentifier, forceDrain, runSoftNavOverSpa = false) {
80
80
  apiInterface[fnName] = apiCall(prefix, fnName, true, 'api');
81
81
  });
82
82
  apiInterface.addPageAction = apiCall(prefix, 'addPageAction', true, _features.FEATURE_NAMES.genericEvents);
83
+ apiInterface.recordCustomEvent = apiCall(prefix, 'recordCustomEvent', true, _features.FEATURE_NAMES.genericEvents);
83
84
  apiInterface.setPageViewName = function (name, host) {
84
85
  if (typeof name !== 'string') return;
85
86
  if (name.charAt(0) !== '/') name = '/' + name;
@@ -198,7 +199,7 @@ function setAPI(agentIdentifier, forceDrain, runSoftNavOverSpa = false) {
198
199
  function apiCall(prefix, name, notSpa, bufferGroup) {
199
200
  return function () {
200
201
  (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ['API/' + name + '/called'], undefined, _features.FEATURE_NAMES.metrics, instanceEE);
201
- if (bufferGroup) (0, _handle.handle)(prefix + name, [(0, _now.now)(), ...arguments], notSpa ? null : this, bufferGroup, instanceEE); // no bufferGroup means only the SM is emitted
202
+ if (bufferGroup) (0, _handle.handle)(prefix + name, [notSpa ? (0, _now.now)() : performance.now(), ...arguments], notSpa ? null : this, bufferGroup, instanceEE); // no bufferGroup means only the SM is emitted
202
203
  return notSpa ? undefined : this; // returns the InteractionHandle which allows these methods to be chained
203
204
  };
204
205
  }
@@ -33,6 +33,16 @@ class MicroAgentBase {
33
33
  return this.#callMethod('addPageAction', name, attributes);
34
34
  }
35
35
 
36
+ /**
37
+ * Records a custom event with a specified eventType and attributes.
38
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/recordCustomEvent/}
39
+ * @param {string} eventType The eventType to store the event as.
40
+ * @param {object} [attributes] JSON object with one or more key/value pairs. For example: {key:"value"}.
41
+ */
42
+ recordCustomEvent(eventType, attributes) {
43
+ return this.#callMethod('recordCustomEvent', eventType, attributes);
44
+ }
45
+
36
46
  /**
37
47
  * Groups page views to help URL structure or to capture the URL's routing information.
38
48
  * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setpageviewname/}
@@ -1,3 +1,4 @@
1
+ import { FEATURE_FLAGS } from '../../features/generic_events/constants';
1
2
  import { LOG_LEVELS } from '../../features/logging/constants';
2
3
  import { isValidSelector } from '../dom/query-selector';
3
4
  import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS } from '../session/constants';
@@ -7,6 +8,12 @@ import { getModeledObject } from './configurable';
7
8
  const nrMask = '[data-nr-mask]';
8
9
  const model = () => {
9
10
  const hiddenState = {
11
+ feature_flags: [],
12
+ experimental: {
13
+ marks: false,
14
+ measures: false,
15
+ resources: false
16
+ },
10
17
  mask_selector: '*',
11
18
  block_selector: '[data-nr-block]',
12
19
  mask_input_options: {
@@ -44,7 +51,12 @@ const model = () => {
44
51
  cors_use_tracecontext_headers: undefined,
45
52
  allowed_origins: undefined
46
53
  },
47
- feature_flags: [],
54
+ get feature_flags() {
55
+ return hiddenState.feature_flags;
56
+ },
57
+ set feature_flags(val) {
58
+ hiddenState.feature_flags = val;
59
+ },
48
60
  generic_events: {
49
61
  enabled: true,
50
62
  harvestTimeSeconds: 30,
@@ -82,8 +94,32 @@ const model = () => {
82
94
  autoStart: true
83
95
  },
84
96
  performance: {
85
- capture_marks: false,
86
- capture_measures: false // false by default through experimental phase, but flipped to true once GA'd
97
+ get capture_marks() {
98
+ return hiddenState.feature_flags.includes(FEATURE_FLAGS.MARKS) || hiddenState.experimental.marks;
99
+ },
100
+ set capture_marks(val) {
101
+ hiddenState.experimental.marks = val;
102
+ },
103
+ get capture_measures() {
104
+ return hiddenState.feature_flags.includes(FEATURE_FLAGS.MEASURES) || hiddenState.experimental.measures;
105
+ },
106
+ set capture_measures(val) {
107
+ hiddenState.experimental.measures = val;
108
+ },
109
+ resources: {
110
+ // whether to run this subfeature or not in the generic_events feature. false by default through experimental phase, but flipped to true once GA'd
111
+ get enabled() {
112
+ return hiddenState.feature_flags.includes(FEATURE_FLAGS.RESOURCES) || hiddenState.experimental.resources;
113
+ },
114
+ set enabled(val) {
115
+ hiddenState.experimental.resources = val;
116
+ },
117
+ asset_types: [],
118
+ // MDN types to collect, empty array will collect all types
119
+ first_party_domains: [],
120
+ // when included, will decorate the resource as first party if matching
121
+ ignore_newrelic: true // ignore capturing internal agent scripts and harvest calls
122
+ }
87
123
  },
88
124
  privacy: {
89
125
  cookies_enabled: true
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.275.0";
9
+ export const VERSION = "1.277.0";
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.275.0";
9
+ export const VERSION = "1.277.0";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -338,7 +338,7 @@ function subscribeToEvents(agentRef, ee, handler, dt) {
338
338
  if (hasUndefinedHostname(params)) return; // don't bother with XHR of url with no hostname
339
339
 
340
340
  metrics.duration = now() - this.startTime;
341
- if (!this.loadCazptureCalled && xhr.readyState === 4) {
341
+ if (!this.loadCaptureCalled && xhr.readyState === 4) {
342
342
  captureXhrData(this, xhr);
343
343
  } else if (params.status == null) {
344
344
  params.status = 0;
@@ -5,8 +5,8 @@
5
5
  import { stringify } from '../../../common/util/stringify';
6
6
  import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler';
7
7
  import { cleanURL } from '../../../common/url/clean-url';
8
- import { FEATURE_NAME } from '../constants';
9
- import { initialLocation, isBrowserScope } from '../../../common/constants/runtime';
8
+ import { FEATURE_NAME, RESERVED_EVENT_TYPES } from '../constants';
9
+ import { globalScope, initialLocation, isBrowserScope } from '../../../common/constants/runtime';
10
10
  import { AggregateBase } from '../../utils/aggregate-base';
11
11
  import { warn } from '../../../common/util/console';
12
12
  import { now } from '../../../common/timing/now';
@@ -16,6 +16,7 @@ import { applyFnToProps } from '../../../common/util/traverse';
16
16
  import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features';
17
17
  import { UserActionsAggregator } from './user-actions/user-actions-aggregator';
18
18
  import { isIFrameWindow } from '../../../common/dom/iframe';
19
+ import { handle } from '../../../common/event-emitter/handle';
19
20
  export class Aggregate extends AggregateBase {
20
21
  static featureName = FEATURE_NAME;
21
22
  constructor(agentRef) {
@@ -29,12 +30,21 @@ export class Aggregate extends AggregateBase {
29
30
  this.deregisterDrain();
30
31
  return;
31
32
  }
33
+ this.trackSupportabilityMetrics();
34
+ registerHandler('api-recordCustomEvent', (timestamp, eventType, attributes) => {
35
+ if (RESERVED_EVENT_TYPES.includes(eventType)) return warn(46);
36
+ this.addEvent({
37
+ eventType,
38
+ timestamp: this.toEpoch(timestamp),
39
+ ...attributes
40
+ });
41
+ }, this.featureName, this.ee);
32
42
  if (agentRef.init.page_action.enabled) {
33
43
  registerHandler('api-addPageAction', (timestamp, name, attributes) => {
34
44
  this.addEvent({
35
45
  ...attributes,
36
46
  eventType: 'PageAction',
37
- timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)),
47
+ timestamp: this.toEpoch(timestamp),
38
48
  timeSinceLoad: timestamp / 1000,
39
49
  actionName: name,
40
50
  referrerUrl: this.referrerUrl,
@@ -60,7 +70,7 @@ export class Aggregate extends AggregateBase {
60
70
  } = aggregatedUserAction.event;
61
71
  this.addEvent({
62
72
  eventType: 'UserAction',
63
- timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timeStamp)),
73
+ timestamp: this.toEpoch(timeStamp),
64
74
  action: type,
65
75
  actionCount: aggregatedUserAction.count,
66
76
  actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
@@ -70,19 +80,28 @@ export class Aggregate extends AggregateBase {
70
80
  ...(isIFrameWindow(window) && {
71
81
  iframe: true
72
82
  }),
73
- ...(target?.id && {
83
+ ...(canTrustTargetAttribute('id') && {
74
84
  targetId: target.id
75
85
  }),
76
- ...(target?.tagName && {
86
+ ...(canTrustTargetAttribute('tagName') && {
77
87
  targetTag: target.tagName
78
88
  }),
79
- ...(target?.type && {
89
+ ...(canTrustTargetAttribute('type') && {
80
90
  targetType: target.type
81
91
  }),
82
- ...(target?.className && {
92
+ ...(canTrustTargetAttribute('className') && {
83
93
  targetClass: target.className
84
94
  })
85
95
  });
96
+
97
+ /**
98
+ * Only trust attributes that exist on HTML element targets, which excludes the window and the document targets
99
+ * @param {string} attribute The attribute to check for on the target element
100
+ * @returns {boolean} Whether the target element has the attribute and can be trusted
101
+ */
102
+ function canTrustTargetAttribute(attribute) {
103
+ return !!(aggregatedUserAction.selectorPath !== 'window' && aggregatedUserAction.selectorPath !== 'document' && target instanceof HTMLElement && target?.[attribute]);
104
+ }
86
105
  }
87
106
  } catch (e) {
88
107
  // do nothing for now
@@ -109,10 +128,11 @@ export class Aggregate extends AggregateBase {
109
128
  const observer = new PerformanceObserver(list => {
110
129
  list.getEntries().forEach(entry => {
111
130
  try {
131
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/Performance/' + type + '/Seen']);
112
132
  this.addEvent({
113
133
  eventType: 'BrowserPerformance',
114
- timestamp: Math.floor(agentRef.runtime.timeKeeper.correctRelativeTimestamp(entry.startTime)),
115
- entryName: entry.name,
134
+ timestamp: this.toEpoch(entry.startTime),
135
+ entryName: cleanURL(entry.name),
116
136
  entryDuration: entry.duration,
117
137
  entryType: type,
118
138
  ...(entry.detail && {
@@ -132,6 +152,47 @@ export class Aggregate extends AggregateBase {
132
152
  // Something failed in our set up, likely the browser does not support PO's... do nothing
133
153
  }
134
154
  }
155
+ if (isBrowserScope && agentRef.init.performance.resources.enabled) {
156
+ registerHandler('browserPerformance.resource', entry => {
157
+ try {
158
+ // convert the entry to a plain object and separate the name and duration from the object
159
+ // you need to do this to be able to spread it into the addEvent call later, and name and duration
160
+ // would be duplicative of entryName and entryDuration and are protected keys in NR1
161
+ const {
162
+ name,
163
+ duration,
164
+ ...entryObject
165
+ } = entry.toJSON();
166
+ let firstParty = false;
167
+ try {
168
+ const entryDomain = new URL(name).hostname;
169
+ const isNr = entryDomain.includes('newrelic.com') || entryDomain.includes('nr-data.net') || entryDomain.includes('nr-local.net');
170
+ /** decide if we should ignore nr-specific assets */
171
+ if (this.agentRef.init.performance.resources.ignore_newrelic && isNr) return;
172
+ /** decide if we should ignore the asset type (empty means allow everything, which is the default) */
173
+ if (this.agentRef.init.performance.resources.asset_types.length && !this.agentRef.init.performance.resources.asset_types.includes(entryObject.initiatorType)) return;
174
+ /** decide if the entryDomain is a first party domain */
175
+ firstParty = entryDomain === globalScope?.location.hostname || agentRef.init.performance.resources.first_party_domains.includes(entryDomain);
176
+ if (firstParty) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/Performance/FirstPartyResource/Seen']);
177
+ if (isNr) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/Performance/NrResource/Seen']);
178
+ } catch (err) {
179
+ // couldnt parse the URL, so firstParty will just default to false
180
+ }
181
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/Performance/Resource/Seen']);
182
+ const event = {
183
+ ...entryObject,
184
+ eventType: 'BrowserPerformance',
185
+ timestamp: Math.floor(agentRef.runtime.timeKeeper.correctRelativeTimestamp(entryObject.startTime)),
186
+ entryName: name,
187
+ entryDuration: duration,
188
+ firstParty
189
+ };
190
+ this.addEvent(event);
191
+ } catch (err) {
192
+ this.ee.emit('internal-error', [err, 'GenericEvents-Resource']);
193
+ }
194
+ }, this.featureName, this.ee);
195
+ }
135
196
  this.harvestScheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
136
197
  onFinished: result => this.postHarvestCleanup(result.sent && result.retry),
137
198
  onUnload: () => addUserAction?.(this.userActionAggregator.aggregationEvent)
@@ -202,4 +263,17 @@ export class Aggregate extends AggregateBase {
202
263
  at: this.agentRef.info.atts
203
264
  };
204
265
  }
266
+ toEpoch(timestamp) {
267
+ return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp));
268
+ }
269
+ trackSupportabilityMetrics() {
270
+ /** track usage SMs to improve these experimental features */
271
+ const configPerfTag = 'Config/Performance/';
272
+ if (this.agentRef.init.performance.capture_marks) handle(SUPPORTABILITY_METRIC_CHANNEL, [configPerfTag + 'CaptureMarks/Enabled']);
273
+ if (this.agentRef.init.performance.capture_measures) handle(SUPPORTABILITY_METRIC_CHANNEL, [configPerfTag + 'CaptureMeasures/Enabled']);
274
+ if (this.agentRef.init.performance.resources.enabled) handle(SUPPORTABILITY_METRIC_CHANNEL, [configPerfTag + 'Resources/Enabled']);
275
+ if (this.agentRef.init.performance.resources.asset_types?.length !== 0) handle(SUPPORTABILITY_METRIC_CHANNEL, [configPerfTag + 'Resources/AssetTypes/Changed']);
276
+ if (this.agentRef.init.performance.resources.first_party_domains?.length !== 0) handle(SUPPORTABILITY_METRIC_CHANNEL, [configPerfTag + 'Resources/FirstPartyDomains/Changed']);
277
+ if (this.agentRef.init.performance.resources.ignore_newrelic === false) handle(SUPPORTABILITY_METRIC_CHANNEL, [configPerfTag + 'Resources/IgnoreNewrelic/Changed']);
278
+ }
205
279
  }
@@ -5,4 +5,10 @@ export const MAX_PAYLOAD_SIZE = 1000000;
5
5
  export const OBSERVED_EVENTS = ['auxclick', 'click', 'copy', 'keydown', 'paste', 'scrollend'];
6
6
  export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur'];
7
7
  export const RAGE_CLICK_THRESHOLD_EVENTS = 4;
8
- export const RAGE_CLICK_THRESHOLD_MS = 1000;
8
+ export const RAGE_CLICK_THRESHOLD_MS = 1000;
9
+ export const RESERVED_EVENT_TYPES = ['PageAction', 'UserAction', 'BrowserPerformance'];
10
+ export const FEATURE_FLAGS = {
11
+ MARKS: 'experimental.marks',
12
+ MEASURES: 'experimental.measures',
13
+ RESOURCES: 'experimental.resources'
14
+ };
@@ -2,7 +2,7 @@
2
2
  * SPDX-License-Identifier: Apache-2.0
3
3
  */
4
4
 
5
- import { isBrowserScope } from '../../../common/constants/runtime';
5
+ import { globalScope, isBrowserScope } from '../../../common/constants/runtime';
6
6
  import { handle } from '../../../common/event-emitter/handle';
7
7
  import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts';
8
8
  import { InstrumentBase } from '../../utils/instrument-base';
@@ -11,14 +11,26 @@ export class Instrument extends InstrumentBase {
11
11
  static featureName = FEATURE_NAME;
12
12
  constructor(agentRef, auto = true) {
13
13
  super(agentRef, FEATURE_NAME, auto);
14
- const genericEventSourceConfigs = [agentRef.init.page_action.enabled, agentRef.init.performance.capture_marks, agentRef.init.performance.capture_measures, agentRef.init.user_actions.enabled
15
- // other future generic event source configs to go here, like M&Ms, PageResouce, etc.
16
- ];
17
- if (isBrowserScope && agentRef.init.user_actions.enabled) {
18
- OBSERVED_EVENTS.forEach(eventType => windowAddEventListener(eventType, evt => handle('ua', [evt], undefined, this.featureName, this.ee), true));
19
- OBSERVED_WINDOW_EVENTS.forEach(eventType => windowAddEventListener(eventType, evt => handle('ua', [evt], undefined, this.featureName, this.ee))
20
- // Capture is not used here so that we don't get element focus/blur events, only the window's as they do not bubble. They are also not cancellable, so no worries about being front of line.
21
- );
14
+ /** config values that gate whether the generic events aggregator should be imported at all */
15
+ const genericEventSourceConfigs = [agentRef.init.page_action.enabled, agentRef.init.performance.capture_marks, agentRef.init.performance.capture_measures, agentRef.init.user_actions.enabled, agentRef.init.performance.resources.enabled];
16
+ if (isBrowserScope) {
17
+ if (agentRef.init.user_actions.enabled) {
18
+ OBSERVED_EVENTS.forEach(eventType => windowAddEventListener(eventType, evt => handle('ua', [evt], undefined, this.featureName, this.ee), true));
19
+ OBSERVED_WINDOW_EVENTS.forEach(eventType => windowAddEventListener(eventType, evt => handle('ua', [evt], undefined, this.featureName, this.ee))
20
+ // Capture is not used here so that we don't get element focus/blur events, only the window's as they do not bubble. They are also not cancellable, so no worries about being front of line.
21
+ );
22
+ }
23
+ if (agentRef.init.performance.resources.enabled && globalScope.PerformanceObserver?.supportedEntryTypes.includes('resource')) {
24
+ const observer = new PerformanceObserver(list => {
25
+ list.getEntries().forEach(entry => {
26
+ handle('browserPerformance.resource', [entry], undefined, this.featureName, this.ee);
27
+ });
28
+ });
29
+ observer.observe({
30
+ type: 'resource',
31
+ buffered: true
32
+ });
33
+ }
22
34
  }
23
35
 
24
36
  /** If any of the sources are active, import the aggregator. otherwise deregister */
@@ -107,9 +107,10 @@ export class Aggregate extends AggregateBase {
107
107
  * @param {boolean=} internal if the error was "caught" and deemed "internal" before reporting to the jserrors feature
108
108
  * @param {object=} customAttributes any custom attributes to be included in the error payload
109
109
  * @param {boolean=} hasReplay a flag indicating if the error occurred during a replay session
110
+ * @param {string=} swallowReason a string indicating pre-defined reason if swallowing the error. Mainly used by the internal error SMs.
110
111
  * @returns
111
112
  */
112
- storeError(err, time, internal, customAttributes, hasReplay) {
113
+ storeError(err, time, internal, customAttributes, hasReplay, swallowReason) {
113
114
  if (!err) return;
114
115
  // are we in an interaction
115
116
  time = time || now();
@@ -127,7 +128,7 @@ export class Aggregate extends AggregateBase {
127
128
  const {
128
129
  shouldSwallow,
129
130
  reason
130
- } = evaluateInternalError(stackInfo, internal);
131
+ } = evaluateInternalError(stackInfo, internal, swallowReason);
131
132
  if (shouldSwallow) {
132
133
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Internal/Error/' + reason], undefined, FEATURE_NAMES.metrics, this.ee);
133
134
  return;
@@ -5,10 +5,10 @@ const REASON_SECURITY_POLICY = 'Security-Policy';
5
5
  * @param {Object} stackInfo - The error stack information.
6
6
  * @returns {boolean} - Whether the error should be swallowed or not.
7
7
  */
8
- export function evaluateInternalError(stackInfo, internal) {
8
+ export function evaluateInternalError(stackInfo, internal, reason) {
9
9
  const output = {
10
10
  shouldSwallow: internal || false,
11
- reason: 'Other'
11
+ reason: reason || 'Other'
12
12
  };
13
13
  const leadingFrame = stackInfo.frames?.[0];
14
14
  /** If we cant otherwise determine from the frames and message, the default of internal + reason will be the fallback */
@@ -20,9 +20,9 @@ export class Instrument extends InstrumentBase {
20
20
  // this try-catch can be removed when IE11 is completely unsupported & gone
21
21
  this.removeOnAbort = new AbortController();
22
22
  } catch (e) {}
23
- this.ee.on('internal-error', error => {
23
+ this.ee.on('internal-error', (error, reason) => {
24
24
  if (!this.abortHandler) return;
25
- handle('ierr', [castError(error), now(), true, {}, this.#replayRunning], undefined, this.featureName, this.ee);
25
+ handle('ierr', [castError(error), now(), true, {}, this.#replayRunning, reason], undefined, this.featureName, this.ee);
26
26
  });
27
27
  this.ee.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, isRunning => {
28
28
  this.#replayRunning = isRunning;
@@ -148,40 +148,6 @@ export class Aggregate extends AggregateBase {
148
148
  });
149
149
  }
150
150
  unload() {
151
- try {
152
- if (this.resourcesSent) return;
153
- this.resourcesSent = true; // make sure this only gets sent once
154
-
155
- // Capture SMs around network resources using the performance API to assess
156
- // work to split this out from the ST nodes
157
- // differentiate between internal+external and ajax+non-ajax
158
- const ajaxResources = ['beacon', 'fetch', 'xmlhttprequest'];
159
- const internalUrls = ['nr-data.net', 'newrelic.com', 'nr-local.net', 'localhost'];
160
- function isInternal(x) {
161
- return internalUrls.some(y => x.name.indexOf(y) >= 0);
162
- }
163
- function isAjax(x) {
164
- return ajaxResources.includes(x.initiatorType);
165
- }
166
- const allResources = performance?.getEntriesByType('resource') || [];
167
- allResources.forEach(entry => {
168
- if (isInternal(entry)) {
169
- if (isAjax(entry)) this.storeSupportabilityMetrics('Generic/Resources/Ajax/Internal');else this.storeSupportabilityMetrics('Generic/Resources/Non-Ajax/Internal');
170
- } else {
171
- if (isAjax(entry)) this.storeSupportabilityMetrics('Generic/Resources/Ajax/External');else this.storeSupportabilityMetrics('Generic/Resources/Non-Ajax/External');
172
- }
173
- });
174
-
175
- // Capture SMs for performance markers and measures to assess the usage and possible inclusion of this
176
- // data in the agent for use in NR
177
- if (typeof performance !== 'undefined') {
178
- const markers = performance.getEntriesByType('mark');
179
- const measures = performance.getEntriesByType('measure');
180
- if (markers.length) this.storeSupportabilityMetrics('Generic/Performance/Mark/Seen', markers.length);
181
- if (measures.length) this.storeSupportabilityMetrics('Generic/Performance/Measure/Seen', measures.length);
182
- }
183
- } catch (e) {
184
- // do nothing
185
- }
151
+ // do nothing for now, marks and measures and resources stats are now being captured by the ge feature
186
152
  }
187
153
  }