@newrelic/browser-agent 1.276.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 (48) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/common/constants/env.cdn.js +1 -1
  3. package/dist/cjs/common/constants/env.npm.js +1 -1
  4. package/dist/cjs/features/generic_events/aggregate/index.js +14 -3
  5. package/dist/cjs/features/generic_events/constants.js +2 -1
  6. package/dist/cjs/features/soft_navigations/aggregate/index.js +28 -11
  7. package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +2 -1
  8. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +12 -12
  9. package/dist/cjs/features/soft_navigations/constants.js +5 -2
  10. package/dist/cjs/loaders/api/api-methods.js +1 -1
  11. package/dist/cjs/loaders/api/api.js +2 -1
  12. package/dist/cjs/loaders/micro-agent-base.js +10 -0
  13. package/dist/esm/common/constants/env.cdn.js +1 -1
  14. package/dist/esm/common/constants/env.npm.js +1 -1
  15. package/dist/esm/features/generic_events/aggregate/index.js +15 -4
  16. package/dist/esm/features/generic_events/constants.js +1 -0
  17. package/dist/esm/features/soft_navigations/aggregate/index.js +29 -12
  18. package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +2 -1
  19. package/dist/esm/features/soft_navigations/aggregate/interaction.js +13 -13
  20. package/dist/esm/features/soft_navigations/constants.js +4 -1
  21. package/dist/esm/loaders/api/api-methods.js +1 -1
  22. package/dist/esm/loaders/api/api.js +2 -1
  23. package/dist/esm/loaders/micro-agent-base.js +10 -0
  24. package/dist/types/features/generic_events/aggregate/index.d.ts +1 -0
  25. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  26. package/dist/types/features/generic_events/constants.d.ts +1 -0
  27. package/dist/types/features/generic_events/constants.d.ts.map +1 -1
  28. package/dist/types/features/soft_navigations/aggregate/index.d.ts +1 -0
  29. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
  30. package/dist/types/features/soft_navigations/aggregate/initial-page-load-interaction.d.ts.map +1 -1
  31. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +3 -3
  32. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  33. package/dist/types/features/soft_navigations/constants.d.ts +1 -0
  34. package/dist/types/features/soft_navigations/constants.d.ts.map +1 -1
  35. package/dist/types/loaders/api/api.d.ts +1 -0
  36. package/dist/types/loaders/api/api.d.ts.map +1 -1
  37. package/dist/types/loaders/micro-agent-base.d.ts +7 -0
  38. package/dist/types/loaders/micro-agent-base.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/features/generic_events/aggregate/index.js +17 -4
  41. package/src/features/generic_events/constants.js +2 -0
  42. package/src/features/soft_navigations/aggregate/index.js +22 -11
  43. package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +2 -1
  44. package/src/features/soft_navigations/aggregate/interaction.js +13 -12
  45. package/src/features/soft_navigations/constants.js +3 -1
  46. package/src/loaders/api/api-methods.js +1 -1
  47. package/src/loaders/api/api.js +3 -1
  48. package/src/loaders/micro-agent-base.js +10 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.277.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.276.0...v1.277.0) (2024-12-18)
7
+
8
+
9
+ ### Features
10
+
11
+ * Add custom events API ([#1263](https://github.com/newrelic/newrelic-browser-agent/issues/1263)) ([9395415](https://github.com/newrelic/newrelic-browser-agent/commit/9395415d942b55e88e89438aa203c6a1642d9e6b))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * Soft navigation bug fixes and new soft navigation tests ([#1268](https://github.com/newrelic/newrelic-browser-agent/issues/1268)) ([7624928](https://github.com/newrelic/newrelic-browser-agent/commit/762492896a7b96564269aab1aadeb6e44a4da242))
17
+
6
18
  ## [1.276.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.275.0...v1.276.0) (2024-12-16)
7
19
 
8
20
 
@@ -12,7 +12,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
12
12
  /**
13
13
  * Exposes the version of the agent
14
14
  */
15
- const VERSION = exports.VERSION = "1.276.0";
15
+ const VERSION = exports.VERSION = "1.277.0";
16
16
 
17
17
  /**
18
18
  * Exposes the build type of the agent
@@ -12,7 +12,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
12
12
  /**
13
13
  * Exposes the version of the agent
14
14
  */
15
- const VERSION = exports.VERSION = "1.276.0";
15
+ const VERSION = exports.VERSION = "1.277.0";
16
16
 
17
17
  /**
18
18
  * Exposes the build type of the agent
@@ -38,12 +38,20 @@ class Aggregate extends _aggregateBase.AggregateBase {
38
38
  return;
39
39
  }
40
40
  this.trackSupportabilityMetrics();
41
+ (0, _registerHandler.registerHandler)('api-recordCustomEvent', (timestamp, eventType, attributes) => {
42
+ if (_constants.RESERVED_EVENT_TYPES.includes(eventType)) return (0, _console.warn)(46);
43
+ this.addEvent({
44
+ eventType,
45
+ timestamp: this.toEpoch(timestamp),
46
+ ...attributes
47
+ });
48
+ }, this.featureName, this.ee);
41
49
  if (agentRef.init.page_action.enabled) {
42
50
  (0, _registerHandler.registerHandler)('api-addPageAction', (timestamp, name, attributes) => {
43
51
  this.addEvent({
44
52
  ...attributes,
45
53
  eventType: 'PageAction',
46
- timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)),
54
+ timestamp: this.toEpoch(timestamp),
47
55
  timeSinceLoad: timestamp / 1000,
48
56
  actionName: name,
49
57
  referrerUrl: this.referrerUrl,
@@ -69,7 +77,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
69
77
  } = aggregatedUserAction.event;
70
78
  this.addEvent({
71
79
  eventType: 'UserAction',
72
- timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timeStamp)),
80
+ timestamp: this.toEpoch(timeStamp),
73
81
  action: type,
74
82
  actionCount: aggregatedUserAction.count,
75
83
  actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
@@ -130,7 +138,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
130
138
  (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['Generic/Performance/' + type + '/Seen']);
131
139
  this.addEvent({
132
140
  eventType: 'BrowserPerformance',
133
- timestamp: Math.floor(agentRef.runtime.timeKeeper.correctRelativeTimestamp(entry.startTime)),
141
+ timestamp: this.toEpoch(entry.startTime),
134
142
  entryName: (0, _cleanUrl.cleanURL)(entry.name),
135
143
  entryDuration: entry.duration,
136
144
  entryType: type,
@@ -262,6 +270,9 @@ class Aggregate extends _aggregateBase.AggregateBase {
262
270
  at: this.agentRef.info.atts
263
271
  };
264
272
  }
273
+ toEpoch(timestamp) {
274
+ return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp));
275
+ }
265
276
  trackSupportabilityMetrics() {
266
277
  /** track usage SMs to improve these experimental features */
267
278
  const configPerfTag = 'Config/Performance/';
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.RAGE_CLICK_THRESHOLD_MS = exports.RAGE_CLICK_THRESHOLD_EVENTS = exports.OBSERVED_WINDOW_EVENTS = exports.OBSERVED_EVENTS = exports.MAX_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = exports.FEATURE_NAME = exports.FEATURE_FLAGS = void 0;
6
+ exports.RESERVED_EVENT_TYPES = exports.RAGE_CLICK_THRESHOLD_MS = exports.RAGE_CLICK_THRESHOLD_EVENTS = exports.OBSERVED_WINDOW_EVENTS = exports.OBSERVED_EVENTS = exports.MAX_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = exports.FEATURE_NAME = exports.FEATURE_FLAGS = void 0;
7
7
  var _features = require("../../loaders/features/features");
8
8
  const FEATURE_NAME = exports.FEATURE_NAME = _features.FEATURE_NAMES.genericEvents;
9
9
  const IDEAL_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = 64000;
@@ -12,6 +12,7 @@ const OBSERVED_EVENTS = exports.OBSERVED_EVENTS = ['auxclick', 'click', 'copy',
12
12
  const OBSERVED_WINDOW_EVENTS = exports.OBSERVED_WINDOW_EVENTS = ['focus', 'blur'];
13
13
  const RAGE_CLICK_THRESHOLD_EVENTS = exports.RAGE_CLICK_THRESHOLD_EVENTS = 4;
14
14
  const RAGE_CLICK_THRESHOLD_MS = exports.RAGE_CLICK_THRESHOLD_MS = 1000;
15
+ const RESERVED_EVENT_TYPES = exports.RESERVED_EVENT_TYPES = ['PageAction', 'UserAction', 'BrowserPerformance'];
15
16
  const FEATURE_FLAGS = exports.FEATURE_FLAGS = {
16
17
  MARKS: 'experimental.marks',
17
18
  MEASURES: 'experimental.measures',
@@ -26,20 +26,23 @@ class Aggregate extends _aggregateBase.AggregateBase {
26
26
  this.interactionsToHarvest = this.events;
27
27
  this.domObserver = domObserver;
28
28
  this.initialPageLoadInteraction = new _initialPageLoadInteraction.InitialPageLoadInteraction(agentRef.agentIdentifier);
29
+ this.initialPageLoadInteraction.onDone.push(() => {
30
+ // this ensures the .end() method also works with iPL
31
+ this.initialPageLoadInteraction.forceSave = true; // unless forcibly ignored, iPL always finish by default
32
+ this.interactionsToHarvest.add(this.initialPageLoadInteraction);
33
+ this.initialPageLoadInteraction = null;
34
+ });
29
35
  _timeToFirstByte.timeToFirstByte.subscribe(({
30
36
  attrs
31
37
  }) => {
32
38
  const loadEventTime = attrs.navigationEntry.loadEventEnd;
33
- this.initialPageLoadInteraction.forceSave = true;
34
39
  this.initialPageLoadInteraction.done(loadEventTime);
35
- this.interactionsToHarvest.add(this.initialPageLoadInteraction);
36
- this.initialPageLoadInteraction = null;
37
40
  // Report metric on the initial page load time
38
41
  (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ['SoftNav/Interaction/InitialPageLoad/Duration/Ms', Math.round(loadEventTime)], undefined, _features.FEATURE_NAMES.metrics, this.ee);
39
42
  });
40
43
  this.latestRouteSetByApi = null;
41
44
  this.interactionInProgress = null; // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
42
-
45
+ this.latestHistoryUrl = null;
43
46
  this.blocked = false;
44
47
  this.waitForFlags(['spa']).then(([spaOn]) => {
45
48
  if (spaOn) {
@@ -59,7 +62,11 @@ class Aggregate extends _aggregateBase.AggregateBase {
59
62
 
60
63
  // By default, a complete UI driven interaction requires event -> URL change -> DOM mod in that exact order.
61
64
  (0, _registerHandler.registerHandler)('newUIEvent', event => this.startUIInteraction(event.type, Math.floor(event.timeStamp), event.target), this.featureName, this.ee);
62
- (0, _registerHandler.registerHandler)('newURL', (timestamp, url) => this.interactionInProgress?.updateHistory(timestamp, url), this.featureName, this.ee);
65
+ (0, _registerHandler.registerHandler)('newURL', (timestamp, url) => {
66
+ // In the case of 'popstate' trigger, by the time the event fires, the URL has already changed, so we need to store what-will-be the *previous* URL for oldURL of next popstate ixn.
67
+ this.latestHistoryUrl = url;
68
+ this.interactionInProgress?.updateHistory(timestamp, url);
69
+ }, this.featureName, this.ee);
63
70
  (0, _registerHandler.registerHandler)('newDom', timestamp => {
64
71
  this.interactionInProgress?.updateDom(timestamp);
65
72
  if (this.interactionInProgress?.seenHistoryAndDomChange()) this.interactionInProgress.done();
@@ -71,20 +78,23 @@ class Aggregate extends _aggregateBase.AggregateBase {
71
78
  serializer(eventBuffer) {
72
79
  // The payload depacker takes the first ixn of a payload (if there are multiple ixns) and positively offset the subsequent ixns timestamps by that amount.
73
80
  // In order to accurately portray the real start & end times of the 2nd & onward ixns, we hence need to negatively offset their start timestamps with that of the 1st ixn.
74
- let firstIxnStartTime = 0; // the very 1st ixn does not require any offsetting
81
+ let firstIxnStartTime;
75
82
  const serializedIxnList = [];
76
83
  for (const interaction of eventBuffer) {
77
84
  serializedIxnList.push(interaction.serialize(firstIxnStartTime));
78
- if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start);
85
+ if (firstIxnStartTime === undefined) firstIxnStartTime = Math.floor(interaction.start); // careful not to match or overwrite on 0 value!
79
86
  }
80
87
  return "bel.7;".concat(serializedIxnList.join(';'));
81
88
  }
82
89
  startUIInteraction(eventName, startedAt, sourceElem) {
83
90
  // this is throttled by instrumentation so that it isn't excessively called
84
91
  if (this.interactionInProgress?.createdByApi) return; // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
85
- if (this.interactionInProgress?.done() === false) return;
86
- this.interactionInProgress = new _interaction.Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi);
87
- if (eventName === 'click') {
92
+ if (this.interactionInProgress?.done() === false) return; // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
93
+
94
+ const oldURL = eventName === _constants2.INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined; // see related comment in 'newURL' handler above, 'popstate'
95
+ this.interactionInProgress = new _interaction.Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi, oldURL);
96
+ if (eventName === _constants2.INTERACTION_TRIGGERS[0]) {
97
+ // 'click'
88
98
  const sourceElemText = getActionText(sourceElem);
89
99
  if (sourceElemText) this.interactionInProgress.customAttributes.actionText = sourceElemText;
90
100
  }
@@ -130,7 +140,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
130
140
  // reverse search for the latest completed interaction for efficiency
131
141
  const finishedInteraction = interactionsBuffer[idx];
132
142
  if (finishedInteraction.isActiveDuring(timestamp)) {
133
- if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction;
143
+ if (finishedInteraction.trigger !== _constants2.IPL_TRIGGER_NAME) return finishedInteraction;
134
144
  // It's possible that a complete interaction occurs before page is fully loaded, so we need to consider if a route-change ixn may have overlapped this iPL
135
145
  else saveIxn = finishedInteraction;
136
146
  }
@@ -194,9 +204,16 @@ class Aggregate extends _aggregateBase.AggregateBase {
194
204
  // In here, 'this' refers to the EventContext specific to per InteractionHandle instance spawned by each .interaction() api call.
195
205
  // Each api call aka IH instance would therefore retain a reference to either the in-progress interaction *at the time of the call* OR a new api-started interaction.
196
206
  this.associatedInteraction = thisClass.getInteractionFor(time);
207
+ if (this.associatedInteraction?.trigger === _constants2.IPL_TRIGGER_NAME) this.associatedInteraction = null; // the api get-interaction method cannot target IPL
197
208
  if (!this.associatedInteraction) {
198
209
  // This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular seenHistoryAndDomChange process.
199
210
  this.associatedInteraction = thisClass.interactionInProgress = new _interaction.Interaction(thisClass.agentIdentifier, _constants2.API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
211
+ thisClass.domObserver.observe(document.body, {
212
+ attributes: true,
213
+ childList: true,
214
+ subtree: true,
215
+ characterData: true
216
+ }); // start observing for DOM changes like a regular UI-driven interaction
200
217
  thisClass.setClosureHandlers();
201
218
  }
202
219
  if (waitForEnd === true) this.associatedInteraction.keepOpenUntilEndApi = true;
@@ -10,9 +10,10 @@ var _belSerializer = require("../../../common/serialize/bel-serializer");
10
10
  var _firstPaint = require("../../../common/vitals/first-paint");
11
11
  var _firstContentfulPaint = require("../../../common/vitals/first-contentful-paint");
12
12
  var _info = require("../../../common/config/info");
13
+ var _constants = require("../constants");
13
14
  class InitialPageLoadInteraction extends _interaction.Interaction {
14
15
  constructor(agentIdentifier) {
15
- super(agentIdentifier, 'initialPageLoad', 0, null);
16
+ super(agentIdentifier, _constants.IPL_TRIGGER_NAME, 0, null);
16
17
  const agentInfo = (0, _info.getInfo)(agentIdentifier);
17
18
  this.queueTime = agentInfo.queueTime;
18
19
  this.appTime = agentInfo.applicationTime;
@@ -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/}
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.276.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.276.0";
9
+ export const VERSION = "1.277.0";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -5,7 +5,7 @@
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';
8
+ import { FEATURE_NAME, RESERVED_EVENT_TYPES } from '../constants';
9
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';
@@ -31,12 +31,20 @@ export class Aggregate extends AggregateBase {
31
31
  return;
32
32
  }
33
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);
34
42
  if (agentRef.init.page_action.enabled) {
35
43
  registerHandler('api-addPageAction', (timestamp, name, attributes) => {
36
44
  this.addEvent({
37
45
  ...attributes,
38
46
  eventType: 'PageAction',
39
- timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)),
47
+ timestamp: this.toEpoch(timestamp),
40
48
  timeSinceLoad: timestamp / 1000,
41
49
  actionName: name,
42
50
  referrerUrl: this.referrerUrl,
@@ -62,7 +70,7 @@ export class Aggregate extends AggregateBase {
62
70
  } = aggregatedUserAction.event;
63
71
  this.addEvent({
64
72
  eventType: 'UserAction',
65
- timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timeStamp)),
73
+ timestamp: this.toEpoch(timeStamp),
66
74
  action: type,
67
75
  actionCount: aggregatedUserAction.count,
68
76
  actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
@@ -123,7 +131,7 @@ export class Aggregate extends AggregateBase {
123
131
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/Performance/' + type + '/Seen']);
124
132
  this.addEvent({
125
133
  eventType: 'BrowserPerformance',
126
- timestamp: Math.floor(agentRef.runtime.timeKeeper.correctRelativeTimestamp(entry.startTime)),
134
+ timestamp: this.toEpoch(entry.startTime),
127
135
  entryName: cleanURL(entry.name),
128
136
  entryDuration: entry.duration,
129
137
  entryType: type,
@@ -255,6 +263,9 @@ export class Aggregate extends AggregateBase {
255
263
  at: this.agentRef.info.atts
256
264
  };
257
265
  }
266
+ toEpoch(timestamp) {
267
+ return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp));
268
+ }
258
269
  trackSupportabilityMetrics() {
259
270
  /** track usage SMs to improve these experimental features */
260
271
  const configPerfTag = 'Config/Performance/';
@@ -6,6 +6,7 @@ export const OBSERVED_EVENTS = ['auxclick', 'click', 'copy', 'keydown', 'paste',
6
6
  export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur'];
7
7
  export const RAGE_CLICK_THRESHOLD_EVENTS = 4;
8
8
  export const RAGE_CLICK_THRESHOLD_MS = 1000;
9
+ export const RESERVED_EVENT_TYPES = ['PageAction', 'UserAction', 'BrowserPerformance'];
9
10
  export const FEATURE_FLAGS = {
10
11
  MARKS: 'experimental.marks',
11
12
  MEASURES: 'experimental.measures',
@@ -6,7 +6,7 @@ import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte';
6
6
  import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features';
7
7
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
8
8
  import { AggregateBase } from '../../utils/aggregate-base';
9
- import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS } from '../constants';
9
+ import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS, INTERACTION_TRIGGERS, IPL_TRIGGER_NAME } from '../constants';
10
10
  import { AjaxNode } from './ajax-node';
11
11
  import { InitialPageLoadInteraction } from './initial-page-load-interaction';
12
12
  import { Interaction } from './interaction';
@@ -20,20 +20,23 @@ export class Aggregate extends AggregateBase {
20
20
  this.interactionsToHarvest = this.events;
21
21
  this.domObserver = domObserver;
22
22
  this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentRef.agentIdentifier);
23
+ this.initialPageLoadInteraction.onDone.push(() => {
24
+ // this ensures the .end() method also works with iPL
25
+ this.initialPageLoadInteraction.forceSave = true; // unless forcibly ignored, iPL always finish by default
26
+ this.interactionsToHarvest.add(this.initialPageLoadInteraction);
27
+ this.initialPageLoadInteraction = null;
28
+ });
23
29
  timeToFirstByte.subscribe(({
24
30
  attrs
25
31
  }) => {
26
32
  const loadEventTime = attrs.navigationEntry.loadEventEnd;
27
- this.initialPageLoadInteraction.forceSave = true;
28
33
  this.initialPageLoadInteraction.done(loadEventTime);
29
- this.interactionsToHarvest.add(this.initialPageLoadInteraction);
30
- this.initialPageLoadInteraction = null;
31
34
  // Report metric on the initial page load time
32
35
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['SoftNav/Interaction/InitialPageLoad/Duration/Ms', Math.round(loadEventTime)], undefined, FEATURE_NAMES.metrics, this.ee);
33
36
  });
34
37
  this.latestRouteSetByApi = null;
35
38
  this.interactionInProgress = null; // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
36
-
39
+ this.latestHistoryUrl = null;
37
40
  this.blocked = false;
38
41
  this.waitForFlags(['spa']).then(([spaOn]) => {
39
42
  if (spaOn) {
@@ -53,7 +56,11 @@ export class Aggregate extends AggregateBase {
53
56
 
54
57
  // By default, a complete UI driven interaction requires event -> URL change -> DOM mod in that exact order.
55
58
  registerHandler('newUIEvent', event => this.startUIInteraction(event.type, Math.floor(event.timeStamp), event.target), this.featureName, this.ee);
56
- registerHandler('newURL', (timestamp, url) => this.interactionInProgress?.updateHistory(timestamp, url), this.featureName, this.ee);
59
+ registerHandler('newURL', (timestamp, url) => {
60
+ // In the case of 'popstate' trigger, by the time the event fires, the URL has already changed, so we need to store what-will-be the *previous* URL for oldURL of next popstate ixn.
61
+ this.latestHistoryUrl = url;
62
+ this.interactionInProgress?.updateHistory(timestamp, url);
63
+ }, this.featureName, this.ee);
57
64
  registerHandler('newDom', timestamp => {
58
65
  this.interactionInProgress?.updateDom(timestamp);
59
66
  if (this.interactionInProgress?.seenHistoryAndDomChange()) this.interactionInProgress.done();
@@ -65,20 +72,23 @@ export class Aggregate extends AggregateBase {
65
72
  serializer(eventBuffer) {
66
73
  // The payload depacker takes the first ixn of a payload (if there are multiple ixns) and positively offset the subsequent ixns timestamps by that amount.
67
74
  // In order to accurately portray the real start & end times of the 2nd & onward ixns, we hence need to negatively offset their start timestamps with that of the 1st ixn.
68
- let firstIxnStartTime = 0; // the very 1st ixn does not require any offsetting
75
+ let firstIxnStartTime;
69
76
  const serializedIxnList = [];
70
77
  for (const interaction of eventBuffer) {
71
78
  serializedIxnList.push(interaction.serialize(firstIxnStartTime));
72
- if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start);
79
+ if (firstIxnStartTime === undefined) firstIxnStartTime = Math.floor(interaction.start); // careful not to match or overwrite on 0 value!
73
80
  }
74
81
  return "bel.7;".concat(serializedIxnList.join(';'));
75
82
  }
76
83
  startUIInteraction(eventName, startedAt, sourceElem) {
77
84
  // this is throttled by instrumentation so that it isn't excessively called
78
85
  if (this.interactionInProgress?.createdByApi) return; // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
79
- if (this.interactionInProgress?.done() === false) return;
80
- this.interactionInProgress = new Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi);
81
- if (eventName === 'click') {
86
+ if (this.interactionInProgress?.done() === false) return; // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
87
+
88
+ const oldURL = eventName === INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined; // see related comment in 'newURL' handler above, 'popstate'
89
+ this.interactionInProgress = new Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi, oldURL);
90
+ if (eventName === INTERACTION_TRIGGERS[0]) {
91
+ // 'click'
82
92
  const sourceElemText = getActionText(sourceElem);
83
93
  if (sourceElemText) this.interactionInProgress.customAttributes.actionText = sourceElemText;
84
94
  }
@@ -124,7 +134,7 @@ export class Aggregate extends AggregateBase {
124
134
  // reverse search for the latest completed interaction for efficiency
125
135
  const finishedInteraction = interactionsBuffer[idx];
126
136
  if (finishedInteraction.isActiveDuring(timestamp)) {
127
- if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction;
137
+ if (finishedInteraction.trigger !== IPL_TRIGGER_NAME) return finishedInteraction;
128
138
  // It's possible that a complete interaction occurs before page is fully loaded, so we need to consider if a route-change ixn may have overlapped this iPL
129
139
  else saveIxn = finishedInteraction;
130
140
  }
@@ -188,9 +198,16 @@ export class Aggregate extends AggregateBase {
188
198
  // In here, 'this' refers to the EventContext specific to per InteractionHandle instance spawned by each .interaction() api call.
189
199
  // Each api call aka IH instance would therefore retain a reference to either the in-progress interaction *at the time of the call* OR a new api-started interaction.
190
200
  this.associatedInteraction = thisClass.getInteractionFor(time);
201
+ if (this.associatedInteraction?.trigger === IPL_TRIGGER_NAME) this.associatedInteraction = null; // the api get-interaction method cannot target IPL
191
202
  if (!this.associatedInteraction) {
192
203
  // This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular seenHistoryAndDomChange process.
193
204
  this.associatedInteraction = thisClass.interactionInProgress = new Interaction(thisClass.agentIdentifier, API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi);
205
+ thisClass.domObserver.observe(document.body, {
206
+ attributes: true,
207
+ childList: true,
208
+ subtree: true,
209
+ characterData: true
210
+ }); // start observing for DOM changes like a regular UI-driven interaction
194
211
  thisClass.setClosureHandlers();
195
212
  }
196
213
  if (waitForEnd === true) this.associatedInteraction.keepOpenUntilEndApi = true;
@@ -4,9 +4,10 @@ import { numeric } from '../../../common/serialize/bel-serializer';
4
4
  import { firstPaint } from '../../../common/vitals/first-paint';
5
5
  import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint';
6
6
  import { getInfo } from '../../../common/config/info';
7
+ import { IPL_TRIGGER_NAME } from '../constants';
7
8
  export class InitialPageLoadInteraction extends Interaction {
8
9
  constructor(agentIdentifier) {
9
- super(agentIdentifier, 'initialPageLoad', 0, null);
10
+ super(agentIdentifier, IPL_TRIGGER_NAME, 0, null);
10
11
  const agentInfo = getInfo(agentIdentifier);
11
12
  this.queueTime = agentInfo.queueTime;
12
13
  this.appTime = agentInfo.applicationTime;