@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
@@ -4,7 +4,7 @@ import { generateUuid } from '../../../common/ids/unique-id';
4
4
  import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer';
5
5
  import { now } from '../../../common/timing/now';
6
6
  import { cleanURL } from '../../../common/url/clean-url';
7
- import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME } from '../constants';
7
+ import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME, IPL_TRIGGER_NAME } from '../constants';
8
8
  import { BelNode } from './bel-node';
9
9
 
10
10
  /**
@@ -13,8 +13,6 @@ import { BelNode } from './bel-node';
13
13
  export class Interaction extends BelNode {
14
14
  id = generateUuid(); // unique id that is serialized and used to link interactions with errors
15
15
  initialPageURL = initialLocation;
16
- oldURL = '' + globalScope?.location;
17
- newURL = '' + globalScope?.location;
18
16
  customName;
19
17
  customAttributes = {};
20
18
  customDataByApi = {};
@@ -29,7 +27,7 @@ export class Interaction extends BelNode {
29
27
  keepOpenUntilEndApi = false;
30
28
  onDone = [];
31
29
  cancellationTimer;
32
- constructor(agentIdentifier, uiEvent, uiEventTimestamp, currentRouteKnown) {
30
+ constructor(agentIdentifier, uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
33
31
  super(agentIdentifier);
34
32
  this.belType = NODE_TYPE.INTERACTION;
35
33
  this.trigger = uiEvent;
@@ -38,6 +36,7 @@ export class Interaction extends BelNode {
38
36
  this.eventSubscription = new Map([['finished', []], ['cancelled', []]]);
39
37
  this.forceSave = this.forceIgnore = false;
40
38
  if (this.trigger === API_TRIGGER_NAME) this.createdByApi = true;
39
+ this.newURL = this.oldURL = currentUrl || globalScope?.location.href;
41
40
  }
42
41
  updateDom(timestamp) {
43
42
  this.domTimestamp = timestamp || now(); // default timestamp should be precise for accurate isActiveDuring calculations
@@ -57,6 +56,8 @@ export class Interaction extends BelNode {
57
56
  done(customEndTime) {
58
57
  // 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.
59
58
  if (this.keepOpenUntilEndApi && customEndTime === undefined) return false;
59
+ // 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.
60
+ if (this.status !== INTERACTION_STATUS.IP) return true;
60
61
  this.onDone.forEach(apiProvidedCb => apiProvidedCb(this.customDataByApi)); // this interaction's .save or .ignore can still be set by these user provided callbacks for example
61
62
 
62
63
  if (this.forceIgnore) this.#cancel(); // .ignore() always has precedence over save actions
@@ -66,7 +67,6 @@ export class Interaction extends BelNode {
66
67
  return true;
67
68
  }
68
69
  #finish(customEndTime = 0) {
69
- if (this.status !== INTERACTION_STATUS.IP) return; // disallow this call if the ixn is already done aka not in-progress
70
70
  clearTimeout(this.cancellationTimer);
71
71
  this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime);
72
72
  this.customAttributes = {
@@ -80,7 +80,6 @@ export class Interaction extends BelNode {
80
80
  callbacks.forEach(fn => fn());
81
81
  }
82
82
  #cancel() {
83
- if (this.status !== INTERACTION_STATUS.IP) return; // disallow this call if the ixn is already done aka not in-progress
84
83
  clearTimeout(this.cancellationTimer);
85
84
  this.status = INTERACTION_STATUS.CAN;
86
85
 
@@ -97,7 +96,7 @@ export class Interaction extends BelNode {
97
96
  */
98
97
  isActiveDuring(timestamp) {
99
98
  if (this.status === INTERACTION_STATUS.IP) return this.start <= timestamp;
100
- return this.status === INTERACTION_STATUS.FIN && this.start <= timestamp && this.end >= timestamp;
99
+ return this.status === INTERACTION_STATUS.FIN && this.start <= timestamp && this.end > timestamp;
101
100
  }
102
101
 
103
102
  // Following are virtual properties overridden by a subclass:
@@ -105,16 +104,17 @@ export class Interaction extends BelNode {
105
104
  get firstContentfulPaint() {}
106
105
  get navTiming() {}
107
106
  serialize(firstStartTimeOfPayload) {
107
+ const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined;
108
108
  const addString = getAddStringContext(this.agentIdentifier);
109
109
  const nodeList = [];
110
110
  let ixnType;
111
- if (this.trigger === 'initialPageLoad') ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD;else if (this.newURL !== this.oldURL) ixnType = INTERACTION_TYPE.ROUTE_CHANGE;else ixnType = INTERACTION_TYPE.UNSPECIFIED;
111
+ if (this.trigger === IPL_TRIGGER_NAME) ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD;else if (this.newURL !== this.oldURL) ixnType = INTERACTION_TYPE.ROUTE_CHANGE;else ixnType = INTERACTION_TYPE.UNSPECIFIED;
112
112
 
113
113
  // 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.
114
114
  const fields = [numeric(this.belType), 0,
115
115
  // this will be overwritten below with number of attached nodes
116
- numeric(this.start - firstStartTimeOfPayload),
117
- // relative to first node
116
+ numeric(this.start - (isFirstIxnOfPayload ? 0 : firstStartTimeOfPayload)),
117
+ // 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
118
118
  numeric(this.end - this.start),
119
119
  // end -- relative to start
120
120
  numeric(this.callbackEnd),
@@ -125,9 +125,9 @@ export class Interaction extends BelNode {
125
125
  const allAttachedNodes = addCustomAttributes(this.customAttributes || {}, addString); // start with all custom attributes
126
126
  if (getInfo(this.agentIdentifier).atts) allAttachedNodes.push('a,' + addString(getInfo(this.agentIdentifier).atts)); // add apm provided attributes
127
127
  /* Querypack encoder+decoder quirkiness:
128
- - If first ixn node of payload is being processed, we use this node's start to offset. (firstStartTime should be 0--or undefined.)
129
- - Else for subsequent ixn nodes, we use the first ixn node's start to offset. */
130
- this.children.forEach(node => allAttachedNodes.push(node.serialize(firstStartTimeOfPayload || this.start))); // recursively add the serialized string of every child of this (ixn) bel node
128
+ - 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.)
129
+ - 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. */
130
+ 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
131
131
 
132
132
  fields[1] = numeric(allAttachedNodes.length);
133
133
  nodeList.push(fields);
@@ -3,9 +3,12 @@ export const INTERACTION_TRIGGERS = ['click',
3
3
  // e.g. user clicks link or the page back/forward buttons
4
4
  'keydown',
5
5
  // e.g. user presses left and right arrow key to switch between displayed photo gallery
6
- 'submit' // e.g. user clicks submit butotn or presses enter while editing a form field
6
+ 'submit',
7
+ // e.g. user clicks submit butotn or presses enter while editing a form field
8
+ 'popstate' // history api is used to navigate back and forward
7
9
  ];
8
10
  export const API_TRIGGER_NAME = 'api';
11
+ export const IPL_TRIGGER_NAME = 'initialPageLoad';
9
12
  export const FEATURE_NAME = FEATURE_NAMES.softNav;
10
13
  export const INTERACTION_TYPE = {
11
14
  INITIAL_PAGE_LOAD: '',
@@ -1,3 +1,3 @@
1
1
  import { SR_EVENT_EMITTER_TYPES } from '../../features/session_replay/constants';
2
- export const apiMethods = ['setErrorHandler', 'finished', 'addToTrace', 'addRelease', 'addPageAction', 'setCurrentRouteName', 'setPageViewName', 'setCustomAttribute', 'interaction', 'noticeError', 'setUserId', 'setApplicationVersion', 'start', SR_EVENT_EMITTER_TYPES.RECORD, SR_EVENT_EMITTER_TYPES.PAUSE, 'log', 'wrapLogger'];
2
+ export const apiMethods = ['setErrorHandler', 'finished', 'addToTrace', 'addRelease', 'recordCustomEvent', 'addPageAction', 'setCurrentRouteName', 'setPageViewName', 'setCustomAttribute', 'interaction', 'noticeError', 'setUserId', 'setApplicationVersion', 'start', SR_EVENT_EMITTER_TYPES.RECORD, SR_EVENT_EMITTER_TYPES.PAUSE, 'log', 'wrapLogger'];
3
3
  export const asyncApiMethods = ['setErrorHandler', 'finished', 'addToTrace', 'addRelease'];
@@ -72,6 +72,7 @@ export function setAPI(agentIdentifier, forceDrain, runSoftNavOverSpa = false) {
72
72
  apiInterface[fnName] = apiCall(prefix, fnName, true, 'api');
73
73
  });
74
74
  apiInterface.addPageAction = apiCall(prefix, 'addPageAction', true, FEATURE_NAMES.genericEvents);
75
+ apiInterface.recordCustomEvent = apiCall(prefix, 'recordCustomEvent', true, FEATURE_NAMES.genericEvents);
75
76
  apiInterface.setPageViewName = function (name, host) {
76
77
  if (typeof name !== 'string') return;
77
78
  if (name.charAt(0) !== '/') name = '/' + name;
@@ -190,7 +191,7 @@ export function setAPI(agentIdentifier, forceDrain, runSoftNavOverSpa = false) {
190
191
  function apiCall(prefix, name, notSpa, bufferGroup) {
191
192
  return function () {
192
193
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['API/' + name + '/called'], undefined, FEATURE_NAMES.metrics, instanceEE);
193
- if (bufferGroup) handle(prefix + name, [now(), ...arguments], notSpa ? null : this, bufferGroup, instanceEE); // no bufferGroup means only the SM is emitted
194
+ if (bufferGroup) handle(prefix + name, [notSpa ? now() : performance.now(), ...arguments], notSpa ? null : this, bufferGroup, instanceEE); // no bufferGroup means only the SM is emitted
194
195
  return notSpa ? undefined : this; // returns the InteractionHandle which allows these methods to be chained
195
196
  };
196
197
  }
@@ -27,6 +27,16 @@ export class MicroAgentBase {
27
27
  return this.#callMethod('addPageAction', name, attributes);
28
28
  }
29
29
 
30
+ /**
31
+ * Records a custom event with a specified eventType and attributes.
32
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/recordCustomEvent/}
33
+ * @param {string} eventType The eventType to store the event as.
34
+ * @param {object} [attributes] JSON object with one or more key/value pairs. For example: {key:"value"}.
35
+ */
36
+ recordCustomEvent(eventType, attributes) {
37
+ return this.#callMethod('recordCustomEvent', eventType, attributes);
38
+ }
39
+
30
40
  /**
31
41
  * Groups page views to help URL structure or to capture the URL's routing information.
32
42
  * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setpageviewname/}
@@ -24,6 +24,7 @@ export class Aggregate extends AggregateBase {
24
24
  ua: any;
25
25
  at: any;
26
26
  };
27
+ toEpoch(timestamp: any): number;
27
28
  trackSupportabilityMetrics(): void;
28
29
  }
29
30
  import { AggregateBase } from '../../utils/aggregate-base';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAoBA;IACE,2BAAiC;IACjC,2BAsKC;IAnKC,yBAA4B;IAC5B,wBAAyE;IAEzE,gCAAkG;IA8B9F,4CAAuD;IAyHzD,mCAGQ;IASZ;;;;;;;;;;;OAWG;IACH,eAHW,MAAM,YAAC,QA0CjB;IAED,qCAEC;IAED;;;MAEC;IAED,mCASC;CACF;8BA7P6B,4BAA4B;sCAOpB,wCAAwC;iCAX7C,2CAA2C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAoBA;IACE,2BAAiC;IACjC,2BA+KC;IA5KC,yBAA4B;IAC5B,wBAAyE;IAEzE,gCAAkG;IAuC9F,4CAAuD;IAyHzD,mCAGQ;IASZ;;;;;;;;;;;OAWG;IACH,eAHW,MAAM,YAAC,QA0CjB;IAED,qCAEC;IAED;;;MAEC;IAED,gCAEC;IAED,mCASC;CACF;8BA1Q6B,4BAA4B;sCAOpB,wCAAwC;iCAX7C,2CAA2C"}
@@ -5,6 +5,7 @@ export const OBSERVED_EVENTS: string[];
5
5
  export const OBSERVED_WINDOW_EVENTS: string[];
6
6
  export const RAGE_CLICK_THRESHOLD_EVENTS: 4;
7
7
  export const RAGE_CLICK_THRESHOLD_MS: 1000;
8
+ export const RESERVED_EVENT_TYPES: string[];
8
9
  export namespace FEATURE_FLAGS {
9
10
  let MARKS: string;
10
11
  let MEASURES: string;
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/generic_events/constants.js"],"names":[],"mappings":"AAEA,kCAAuD;AACvD,uCAAuC;AACvC,uCAAuC;AAEvC,uCAA6F;AAC7F,8CAAuD;AAEvD,4CAA4C;AAC5C,2CAA2C"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/generic_events/constants.js"],"names":[],"mappings":"AAEA,kCAAuD;AACvD,uCAAuC;AACvC,uCAAuC;AAEvC,uCAA6F;AAC7F,8CAAuD;AAEvD,4CAA4C;AAC5C,2CAA2C;AAE3C,4CAAsF"}
@@ -8,6 +8,7 @@ export class Aggregate extends AggregateBase {
8
8
  initialPageLoadInteraction: InitialPageLoadInteraction;
9
9
  latestRouteSetByApi: any;
10
10
  interactionInProgress: Interaction | null;
11
+ latestHistoryUrl: any;
11
12
  serializer(eventBuffer: any): string;
12
13
  startUIInteraction(eventName: any, startedAt: any, sourceElem: any): void;
13
14
  setClosureHandlers(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/index.js"],"names":[],"mappings":"AAaA;IACE,2BAAiC;IACjC;;OAkDC;IA9CC,2BAAwC;IACxC,iBAA8B;IAE9B,uDAA0F;IAW1F,yBAA+B;IAC/B,0CAAiC;IAiCnC,qCAUC;IAED,0EAeC;IAED,2BAiBC;IAED;;;;;;;OAOG;IACH,6BAHW,mBAAmB,OAqB7B;;CA2FF;8BAjO6B,4BAA4B;2CAGf,iCAAiC;4BAChD,eAAe"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/index.js"],"names":[],"mappings":"AAaA;IACE,2BAAiC;IACjC;;OAyDC;IArDC,2BAAwC;IACxC,iBAA8B;IAE9B,uDAA0F;IAa1F,yBAA+B;IAC/B,0CAAiC;IACjC,sBAA4B;IAqC9B,qCAUC;IAED,0EAiBC;IAED,2BAiBC;IAED;;;;;;;OAOG;IACH,6BAHW,mBAAmB,OAqB7B;;CA6FF;8BA5O6B,4BAA4B;2CAGf,iCAAiC;4BAChD,eAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"initial-page-load-interaction.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/initial-page-load-interaction.js"],"names":[],"mappings":"AAOA;IACE,kCAKC;IAED,sBAAqD;IACrD,gCAAyE;IAEzE;;;OAGG;IACH,oCA6BC;CACF;4BAnD2B,eAAe"}
1
+ {"version":3,"file":"initial-page-load-interaction.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/initial-page-load-interaction.js"],"names":[],"mappings":"AAQA;IACE,kCAKC;IAED,sBAAqD;IACrD,gCAAyE;IAEzE;;;OAGG;IACH,oCA6BC;CACF;4BApD2B,eAAe"}
@@ -2,11 +2,9 @@
2
2
  * link https://github.com/newrelic/nr-querypack/blob/main/schemas/bel/7.qpschema
3
3
  **/
4
4
  export class Interaction extends BelNode {
5
- constructor(agentIdentifier: any, uiEvent: any, uiEventTimestamp: any, currentRouteKnown: any);
5
+ constructor(agentIdentifier: any, uiEvent: any, uiEventTimestamp: any, currentRouteKnown: any, currentUrl: any);
6
6
  id: string;
7
7
  initialPageURL: string;
8
- oldURL: string;
9
- newURL: string;
10
8
  customName: any;
11
9
  customAttributes: {};
12
10
  customDataByApi: {};
@@ -27,6 +25,8 @@ export class Interaction extends BelNode {
27
25
  eventSubscription: Map<string, never[]>;
28
26
  forceSave: boolean;
29
27
  forceIgnore: boolean;
28
+ newURL: any;
29
+ oldURL: any;
30
30
  updateDom(timestamp: any): void;
31
31
  updateHistory(timestamp: any, newUrl: any): void;
32
32
  seenHistoryAndDomChange(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"interaction.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/interaction.js"],"names":[],"mappings":"AASA;;IAEI;AACJ;IAoBE,+FAYC;IA/BD,WAAmB;IACnB,uBAAgC;IAChC,eAAmC;IACnC,eAAmC;IACnC,gBAAU;IACV,qBAAqB;IACrB,oBAAoB;IACpB,eAAS;IACT,aAAO;IACP,cAAQ;IACR,+EAA+E;IAC/E,eAA8B;IAC9B,qBAAgB;IAChB,yBAAoB;IACpB,sBAAoB;IACpB,6BAA2B;IAC3B,cAAW;IACX,uBAAiB;IAIf,gBAAoC;IACpC,aAAsB;IAEtB,cAAiC;IACjC,wCAGE;IACF,mBAAyC;IAAxB,qBAAwB;IAI3C,gCAEC;IAED,iDAGC;IAED,mCAEC;IAED,8BAIC;IAED,kCAUC;IAwBD;;;;;OAKG;IACH,0BAHW,mBAAmB,WAM7B;IAGD,uBAAoB;IACpB,iCAA8B;IAC9B,sBAAmB;IAEnB,gDA2CC;;CACF;wBAxJuB,YAAY"}
1
+ {"version":3,"file":"interaction.d.ts","sourceRoot":"","sources":["../../../../../src/features/soft_navigations/aggregate/interaction.js"],"names":[],"mappings":"AASA;;IAEI;AACJ;IAkBE,gHAaC;IA9BD,WAAmB;IACnB,uBAAgC;IAChC,gBAAU;IACV,qBAAqB;IACrB,oBAAoB;IACpB,eAAS;IACT,aAAO;IACP,cAAQ;IACR,+EAA+E;IAC/E,eAA8B;IAC9B,qBAAgB;IAChB,yBAAoB;IACpB,sBAAoB;IACpB,6BAA2B;IAC3B,cAAW;IACX,uBAAiB;IAIf,gBAAoC;IACpC,aAAsB;IAEtB,cAAiC;IACjC,wCAGE;IACF,mBAAyC;IAAxB,qBAAwB;IAEzC,YAAsE;IAAxD,YAAwD;IAGxE,gCAEC;IAED,iDAGC;IAED,mCAEC;IAED,8BAIC;IAED,kCAaC;IAsBD;;;;;OAKG;IACH,0BAHW,mBAAmB,WAM7B;IAGD,uBAAoB;IACpB,iCAA8B;IAC9B,sBAAmB;IAEnB,gDA4CC;;CACF;wBAzJuB,YAAY"}
@@ -1,5 +1,6 @@
1
1
  export const INTERACTION_TRIGGERS: string[];
2
2
  export const API_TRIGGER_NAME: "api";
3
+ export const IPL_TRIGGER_NAME: "initialPageLoad";
3
4
  export const FEATURE_NAME: string;
4
5
  export namespace INTERACTION_TYPE {
5
6
  let INITIAL_PAGE_LOAD: string;
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/soft_navigations/constants.js"],"names":[],"mappings":"AAEA,4CAIC;AACD,qCAAqC;AAErC,kCAAiD"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/features/soft_navigations/constants.js"],"names":[],"mappings":"AAEA,4CAKC;AACD,qCAAqC;AACrC,iDAAiD;AAEjD,kCAAiD"}
@@ -9,6 +9,7 @@ export function setAPI(agentIdentifier: any, forceDrain: any, runSoftNavOverSpa?
9
9
  level?: string | undefined;
10
10
  }): void;
11
11
  addPageAction: (...args: any[]) => any;
12
+ recordCustomEvent: (...args: any[]) => any;
12
13
  setPageViewName(name: any, host: any): any;
13
14
  setCustomAttribute(name: any, value: any, persistAttribute?: boolean): any;
14
15
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../../src/loaders/api/api.js"],"names":[],"mappings":"AAuBA,2CAiBC;AAID;;;;;;;;;;;;IAiEE;;;;OAIG;qBAFQ,MAAM;IAWjB;;;;OAIG;iCAFQ,MAAM,GAAC,IAAI;;;;;EAiGvB"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../../src/loaders/api/api.js"],"names":[],"mappings":"AAuBA,2CAiBC;AAID;;;;;;;;;;;;;IAmEE;;;;OAIG;qBAFQ,MAAM;IAWjB;;;;OAIG;iCAFQ,MAAM,GAAC,IAAI;;;;;EAiGvB"}
@@ -8,6 +8,13 @@ export class MicroAgentBase {
8
8
  * @param {object} [attributes] JSON object with one or more key/value pairs. For example: {key:"value"}. The key is reported as its own PageAction attribute with the specified values.
9
9
  */
10
10
  addPageAction(name: string, attributes?: object | undefined): any;
11
+ /**
12
+ * Records a custom event with a specified eventType and attributes.
13
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/recordCustomEvent/}
14
+ * @param {string} eventType The eventType to store the event as.
15
+ * @param {object} [attributes] JSON object with one or more key/value pairs. For example: {key:"value"}.
16
+ */
17
+ recordCustomEvent(eventType: string, attributes?: object | undefined): any;
11
18
  /**
12
19
  * Groups page views to help URL structure or to capture the URL's routing information.
13
20
  * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setpageviewname/}
@@ -1 +1 @@
1
- {"version":3,"file":"micro-agent-base.d.ts","sourceRoot":"","sources":["../../../src/loaders/micro-agent-base.js"],"names":[],"mappings":"AAGA;IAGE,sCAEC;IAJD,wBAAe;IAkBf;;;;;OAKG;IACH,oBAHW,MAAM,wCAKhB;IAED;;;;;OAKG;IACH,sBAHW,MAAM,kCAKhB;IAED;;;;;;OAMG;IACH,yBAJW,MAAM,SACN,MAAM,GAAC,MAAM,GAAC,OAAO,GAAC,IAAI,sCAKpC;IAED;;;;;OAKG;IACH,mBAHW,KAAK,GAAC,MAAM,8CAKtB;IAED;;;;OAIG;IACH,iBAFW,MAAM,GAAC,IAAI,OAIrB;IAED;;;;;;;OAOG;IACH,6BAJW,MAAM,GAAC,IAAI,OAMrB;IAED;;;;OAIG;IACH,0BAFW,CAAC,KAAK,EAAE,KAAK,GAAC,MAAM,KAAK,OAAO,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,OAI9D;IAED;;;;;OAKG;IACH,iBAHW,MAAM,MACN,MAAM,OAIhB;IAED;;;;;MAKE;IACF,aAHW,MAAM;2BACc,MAAM;gBAAU,OAAO,GAAC,OAAO,GAAC,OAAO,GAAC,MAAM,GAAC,MAAM;wBAInF;;CACF"}
1
+ {"version":3,"file":"micro-agent-base.d.ts","sourceRoot":"","sources":["../../../src/loaders/micro-agent-base.js"],"names":[],"mappings":"AAGA;IAGE,sCAEC;IAJD,wBAAe;IAkBf;;;;;OAKG;IACH,oBAHW,MAAM,wCAKhB;IAED;;;;;OAKG;IACH,6BAHW,MAAM,wCAKhB;IAED;;;;;OAKG;IACH,sBAHW,MAAM,kCAKhB;IAED;;;;;;OAMG;IACH,yBAJW,MAAM,SACN,MAAM,GAAC,MAAM,GAAC,OAAO,GAAC,IAAI,sCAKpC;IAED;;;;;OAKG;IACH,mBAHW,KAAK,GAAC,MAAM,8CAKtB;IAED;;;;OAIG;IACH,iBAFW,MAAM,GAAC,IAAI,OAIrB;IAED;;;;;;;OAOG;IACH,6BAJW,MAAM,GAAC,IAAI,OAMrB;IAED;;;;OAIG;IACH,0BAFW,CAAC,KAAK,EAAE,KAAK,GAAC,MAAM,KAAK,OAAO,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,OAI9D;IAED;;;;;OAKG;IACH,iBAHW,MAAM,MACN,MAAM,OAIhB;IAED;;;;;MAKE;IACF,aAHW,MAAM;2BACc,MAAM;gBAAU,OAAO,GAAC,OAAO,GAAC,OAAO,GAAC,MAAM,GAAC,MAAM;wBAInF;;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.276.0",
3
+ "version": "1.277.0",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser 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'
@@ -37,12 +37,21 @@ export class Aggregate extends AggregateBase {
37
37
 
38
38
  this.trackSupportabilityMetrics()
39
39
 
40
+ registerHandler('api-recordCustomEvent', (timestamp, eventType, attributes) => {
41
+ if (RESERVED_EVENT_TYPES.includes(eventType)) return warn(46)
42
+ this.addEvent({
43
+ eventType,
44
+ timestamp: this.toEpoch(timestamp),
45
+ ...attributes
46
+ })
47
+ }, this.featureName, this.ee)
48
+
40
49
  if (agentRef.init.page_action.enabled) {
41
50
  registerHandler('api-addPageAction', (timestamp, name, attributes) => {
42
51
  this.addEvent({
43
52
  ...attributes,
44
53
  eventType: 'PageAction',
45
- timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)),
54
+ timestamp: this.toEpoch(timestamp),
46
55
  timeSinceLoad: timestamp / 1000,
47
56
  actionName: name,
48
57
  referrerUrl: this.referrerUrl,
@@ -66,7 +75,7 @@ export class Aggregate extends AggregateBase {
66
75
  const { target, timeStamp, type } = aggregatedUserAction.event
67
76
  this.addEvent({
68
77
  eventType: 'UserAction',
69
- timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timeStamp)),
78
+ timestamp: this.toEpoch(timeStamp),
70
79
  action: type,
71
80
  actionCount: aggregatedUserAction.count,
72
81
  actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
@@ -118,7 +127,7 @@ export class Aggregate extends AggregateBase {
118
127
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/Performance/' + type + '/Seen'])
119
128
  this.addEvent({
120
129
  eventType: 'BrowserPerformance',
121
- timestamp: Math.floor(agentRef.runtime.timeKeeper.correctRelativeTimestamp(entry.startTime)),
130
+ timestamp: this.toEpoch(entry.startTime),
122
131
  entryName: cleanURL(entry.name),
123
132
  entryDuration: entry.duration,
124
133
  entryType: type,
@@ -250,6 +259,10 @@ export class Aggregate extends AggregateBase {
250
259
  return { ua: this.agentRef.info.userAttributes, at: this.agentRef.info.atts }
251
260
  }
252
261
 
262
+ toEpoch (timestamp) {
263
+ return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp))
264
+ }
265
+
253
266
  trackSupportabilityMetrics () {
254
267
  /** track usage SMs to improve these experimental features */
255
268
  const configPerfTag = 'Config/Performance/'
@@ -10,6 +10,8 @@ export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur']
10
10
  export const RAGE_CLICK_THRESHOLD_EVENTS = 4
11
11
  export const RAGE_CLICK_THRESHOLD_MS = 1000
12
12
 
13
+ export const RESERVED_EVENT_TYPES = ['PageAction', 'UserAction', 'BrowserPerformance']
14
+
13
15
  export const FEATURE_FLAGS = {
14
16
  MARKS: 'experimental.marks',
15
17
  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'
@@ -21,18 +21,21 @@ export class Aggregate extends AggregateBase {
21
21
  this.domObserver = domObserver
22
22
 
23
23
  this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentRef.agentIdentifier)
24
+ this.initialPageLoadInteraction.onDone.push(() => { // 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
+ })
24
29
  timeToFirstByte.subscribe(({ attrs }) => {
25
30
  const loadEventTime = attrs.navigationEntry.loadEventEnd
26
- this.initialPageLoadInteraction.forceSave = true
27
31
  this.initialPageLoadInteraction.done(loadEventTime)
28
- this.interactionsToHarvest.add(this.initialPageLoadInteraction)
29
- this.initialPageLoadInteraction = null
30
32
  // Report metric on the initial page load time
31
33
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['SoftNav/Interaction/InitialPageLoad/Duration/Ms', Math.round(loadEventTime)], undefined, FEATURE_NAMES.metrics, this.ee)
32
34
  })
33
35
 
34
36
  this.latestRouteSetByApi = null
35
37
  this.interactionInProgress = null // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
38
+ this.latestHistoryUrl = null
36
39
 
37
40
  this.blocked = false
38
41
  this.waitForFlags(['spa']).then(([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()
@@ -68,21 +75,23 @@ export class Aggregate extends AggregateBase {
68
75
  serializer (eventBuffer) {
69
76
  // 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.
70
77
  // 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.
71
- let firstIxnStartTime = 0 // the very 1st ixn does not require any offsetting
78
+ let firstIxnStartTime
72
79
  const serializedIxnList = []
73
80
  for (const interaction of eventBuffer) {
74
81
  serializedIxnList.push(interaction.serialize(firstIxnStartTime))
75
- if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start)
82
+ if (firstIxnStartTime === undefined) firstIxnStartTime = Math.floor(interaction.start) // careful not to match or overwrite on 0 value!
76
83
  }
77
84
  return `bel.7;${serializedIxnList.join(';')}`
78
85
  }
79
86
 
80
87
  startUIInteraction (eventName, startedAt, sourceElem) { // this is throttled by instrumentation so that it isn't excessively called
81
88
  if (this.interactionInProgress?.createdByApi) return // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
82
- if (this.interactionInProgress?.done() === false) return
89
+ if (this.interactionInProgress?.done() === false) return // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
90
+
91
+ const oldURL = eventName === INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined // see related comment in 'newURL' handler above, 'popstate'
92
+ this.interactionInProgress = new Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi, oldURL)
83
93
 
84
- this.interactionInProgress = new Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi)
85
- if (eventName === 'click') {
94
+ if (eventName === INTERACTION_TRIGGERS[0]) { // 'click'
86
95
  const sourceElemText = getActionText(sourceElem)
87
96
  if (sourceElemText) this.interactionInProgress.customAttributes.actionText = sourceElemText
88
97
  }
@@ -131,7 +140,7 @@ export class Aggregate extends AggregateBase {
131
140
  for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) { // reverse search for the latest completed interaction for efficiency
132
141
  const finishedInteraction = interactionsBuffer[idx]
133
142
  if (finishedInteraction.isActiveDuring(timestamp)) {
134
- if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction
143
+ if (finishedInteraction.trigger !== IPL_TRIGGER_NAME) return finishedInteraction
135
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
136
145
  else saveIxn = finishedInteraction
137
146
  }
@@ -196,9 +205,11 @@ export class Aggregate extends AggregateBase {
196
205
  // In here, 'this' refers to the EventContext specific to per InteractionHandle instance spawned by each .interaction() api call.
197
206
  // 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.
198
207
  this.associatedInteraction = thisClass.getInteractionFor(time)
208
+ if (this.associatedInteraction?.trigger === IPL_TRIGGER_NAME) this.associatedInteraction = null // the api get-interaction method cannot target IPL
199
209
  if (!this.associatedInteraction) {
200
210
  // 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.
201
211
  this.associatedInteraction = thisClass.interactionInProgress = new Interaction(thisClass.agentIdentifier, API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi)
212
+ thisClass.domObserver.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true }) // start observing for DOM changes like a regular UI-driven interaction
202
213
  thisClass.setClosureHandlers()
203
214
  }
204
215
  if (waitForEnd === true) this.associatedInteraction.keepOpenUntilEndApi = true
@@ -4,10 +4,11 @@ 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
 
8
9
  export class InitialPageLoadInteraction extends Interaction {
9
10
  constructor (agentIdentifier) {
10
- super(agentIdentifier, 'initialPageLoad', 0, null)
11
+ super(agentIdentifier, IPL_TRIGGER_NAME, 0, null)
11
12
  const agentInfo = getInfo(agentIdentifier)
12
13
  this.queueTime = agentInfo.queueTime
13
14
  this.appTime = agentInfo.applicationTime
@@ -4,7 +4,7 @@ import { generateUuid } from '../../../common/ids/unique-id'
4
4
  import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer'
5
5
  import { now } from '../../../common/timing/now'
6
6
  import { cleanURL } from '../../../common/url/clean-url'
7
- import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME } from '../constants'
7
+ import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME, IPL_TRIGGER_NAME } from '../constants'
8
8
  import { BelNode } from './bel-node'
9
9
 
10
10
  /**
@@ -13,8 +13,6 @@ import { BelNode } from './bel-node'
13
13
  export class Interaction extends BelNode {
14
14
  id = generateUuid() // unique id that is serialized and used to link interactions with errors
15
15
  initialPageURL = initialLocation
16
- oldURL = '' + globalScope?.location
17
- newURL = '' + globalScope?.location
18
16
  customName
19
17
  customAttributes = {}
20
18
  customDataByApi = {}
@@ -30,7 +28,7 @@ export class Interaction extends BelNode {
30
28
  onDone = []
31
29
  cancellationTimer
32
30
 
33
- constructor (agentIdentifier, uiEvent, uiEventTimestamp, currentRouteKnown) {
31
+ constructor (agentIdentifier, uiEvent, uiEventTimestamp, currentRouteKnown, currentUrl) {
34
32
  super(agentIdentifier)
35
33
  this.belType = NODE_TYPE.INTERACTION
36
34
  this.trigger = uiEvent
@@ -42,6 +40,7 @@ export class Interaction extends BelNode {
42
40
  ])
43
41
  this.forceSave = this.forceIgnore = false
44
42
  if (this.trigger === API_TRIGGER_NAME) this.createdByApi = true
43
+ this.newURL = this.oldURL = (currentUrl || globalScope?.location.href)
45
44
  }
46
45
 
47
46
  updateDom (timestamp) {
@@ -66,6 +65,9 @@ export class Interaction extends BelNode {
66
65
  done (customEndTime) {
67
66
  // 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.
68
67
  if (this.keepOpenUntilEndApi && customEndTime === undefined) return false
68
+ // 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.
69
+ if (this.status !== INTERACTION_STATUS.IP) return true
70
+
69
71
  this.onDone.forEach(apiProvidedCb => apiProvidedCb(this.customDataByApi)) // this interaction's .save or .ignore can still be set by these user provided callbacks for example
70
72
 
71
73
  if (this.forceIgnore) this.#cancel() // .ignore() always has precedence over save actions
@@ -76,7 +78,6 @@ export class Interaction extends BelNode {
76
78
  }
77
79
 
78
80
  #finish (customEndTime = 0) {
79
- if (this.status !== INTERACTION_STATUS.IP) return // disallow this call if the ixn is already done aka not in-progress
80
81
  clearTimeout(this.cancellationTimer)
81
82
  this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime)
82
83
  this.customAttributes = { ...getInfo(this.agentIdentifier).jsAttributes, ...this.customAttributes } // attrs specific to this interaction should have precedence over the general custom attrs
@@ -88,7 +89,6 @@ export class Interaction extends BelNode {
88
89
  }
89
90
 
90
91
  #cancel () {
91
- if (this.status !== INTERACTION_STATUS.IP) return // disallow this call if the ixn is already done aka not in-progress
92
92
  clearTimeout(this.cancellationTimer)
93
93
  this.status = INTERACTION_STATUS.CAN
94
94
 
@@ -105,7 +105,7 @@ export class Interaction extends BelNode {
105
105
  */
106
106
  isActiveDuring (timestamp) {
107
107
  if (this.status === INTERACTION_STATUS.IP) return this.start <= timestamp
108
- return (this.status === INTERACTION_STATUS.FIN && this.start <= timestamp && this.end >= timestamp)
108
+ return (this.status === INTERACTION_STATUS.FIN && this.start <= timestamp && this.end > timestamp)
109
109
  }
110
110
 
111
111
  // Following are virtual properties overridden by a subclass:
@@ -114,10 +114,11 @@ export class Interaction extends BelNode {
114
114
  get navTiming () {}
115
115
 
116
116
  serialize (firstStartTimeOfPayload) {
117
+ const isFirstIxnOfPayload = firstStartTimeOfPayload === undefined
117
118
  const addString = getAddStringContext(this.agentIdentifier)
118
119
  const nodeList = []
119
120
  let ixnType
120
- if (this.trigger === 'initialPageLoad') ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD
121
+ if (this.trigger === IPL_TRIGGER_NAME) ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD
121
122
  else if (this.newURL !== this.oldURL) ixnType = INTERACTION_TYPE.ROUTE_CHANGE
122
123
  else ixnType = INTERACTION_TYPE.UNSPECIFIED
123
124
 
@@ -125,7 +126,7 @@ export class Interaction extends BelNode {
125
126
  const fields = [
126
127
  numeric(this.belType),
127
128
  0, // this will be overwritten below with number of attached nodes
128
- numeric(this.start - firstStartTimeOfPayload), // relative to first node
129
+ numeric(this.start - (isFirstIxnOfPayload ? 0 : firstStartTimeOfPayload)), // 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
129
130
  numeric(this.end - this.start), // end -- relative to start
130
131
  numeric(this.callbackEnd), // cbEnd -- relative to start; not used by BrowserInteraction events
131
132
  numeric(this.callbackDuration), // not relative
@@ -144,9 +145,9 @@ export class Interaction extends BelNode {
144
145
  const allAttachedNodes = addCustomAttributes(this.customAttributes || {}, addString) // start with all custom attributes
145
146
  if (getInfo(this.agentIdentifier).atts) allAttachedNodes.push('a,' + addString(getInfo(this.agentIdentifier).atts)) // add apm provided attributes
146
147
  /* Querypack encoder+decoder quirkiness:
147
- - If first ixn node of payload is being processed, we use this node's start to offset. (firstStartTime should be 0--or undefined.)
148
- - Else for subsequent ixn nodes, we use the first ixn node's start to offset. */
149
- this.children.forEach(node => allAttachedNodes.push(node.serialize(firstStartTimeOfPayload || this.start))) // recursively add the serialized string of every child of this (ixn) bel node
148
+ - 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.)
149
+ - 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. */
150
+ 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
150
151
 
151
152
  fields[1] = numeric(allAttachedNodes.length)
152
153
  nodeList.push(fields)
@@ -3,9 +3,11 @@ import { FEATURE_NAMES } from '../../loaders/features/features'
3
3
  export const INTERACTION_TRIGGERS = [
4
4
  'click', // e.g. user clicks link or the page back/forward buttons
5
5
  'keydown', // e.g. user presses left and right arrow key to switch between displayed photo gallery
6
- 'submit' // e.g. user clicks submit butotn or presses enter while editing a form field
6
+ 'submit', // e.g. user clicks submit butotn or presses enter while editing a form field
7
+ 'popstate' // history api is used to navigate back and forward
7
8
  ]
8
9
  export const API_TRIGGER_NAME = 'api'
10
+ export const IPL_TRIGGER_NAME = 'initialPageLoad'
9
11
 
10
12
  export const FEATURE_NAME = FEATURE_NAMES.softNav
11
13
 
@@ -1,7 +1,7 @@
1
1
  import { SR_EVENT_EMITTER_TYPES } from '../../features/session_replay/constants'
2
2
 
3
3
  export const apiMethods = [
4
- 'setErrorHandler', 'finished', 'addToTrace', 'addRelease',
4
+ 'setErrorHandler', 'finished', 'addToTrace', 'addRelease', 'recordCustomEvent',
5
5
  'addPageAction', 'setCurrentRouteName', 'setPageViewName', 'setCustomAttribute',
6
6
  'interaction', 'noticeError', 'setUserId', 'setApplicationVersion', 'start',
7
7
  SR_EVENT_EMITTER_TYPES.RECORD, SR_EVENT_EMITTER_TYPES.PAUSE, 'log', 'wrapLogger'