@newrelic/browser-agent 1.267.0 → 1.268.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 (82) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/common/config/init.js +3 -0
  3. package/dist/cjs/common/constants/env.cdn.js +1 -1
  4. package/dist/cjs/common/constants/env.npm.js +1 -1
  5. package/dist/cjs/common/dom/iframe.js +10 -0
  6. package/dist/cjs/common/dom/selector-path.js +48 -0
  7. package/dist/cjs/common/event-listener/event-listener-opts.js +4 -26
  8. package/dist/cjs/common/timing/time-keeper.js +9 -0
  9. package/dist/cjs/common/util/stringify.js +1 -1
  10. package/dist/cjs/features/generic_events/aggregate/index.js +80 -9
  11. package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +39 -0
  12. package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +77 -0
  13. package/dist/cjs/features/generic_events/constants.js +6 -2
  14. package/dist/cjs/features/generic_events/instrument/index.js +12 -1
  15. package/dist/cjs/features/jserrors/aggregate/index.js +2 -2
  16. package/dist/cjs/features/logging/aggregate/index.js +1 -1
  17. package/dist/cjs/features/metrics/aggregate/index.js +2 -1
  18. package/dist/cjs/features/page_view_event/aggregate/index.js +1 -1
  19. package/dist/cjs/features/session_replay/aggregate/index.js +5 -3
  20. package/dist/cjs/features/session_trace/aggregate/index.js +6 -4
  21. package/dist/esm/common/config/init.js +3 -0
  22. package/dist/esm/common/constants/env.cdn.js +1 -1
  23. package/dist/esm/common/constants/env.npm.js +1 -1
  24. package/dist/esm/common/dom/iframe.js +4 -0
  25. package/dist/esm/common/dom/selector-path.js +41 -0
  26. package/dist/esm/common/event-listener/event-listener-opts.js +4 -27
  27. package/dist/esm/common/timing/time-keeper.js +9 -0
  28. package/dist/esm/common/util/stringify.js +1 -1
  29. package/dist/esm/features/generic_events/aggregate/index.js +82 -11
  30. package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +32 -0
  31. package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +70 -0
  32. package/dist/esm/features/generic_events/constants.js +5 -1
  33. package/dist/esm/features/generic_events/instrument/index.js +14 -3
  34. package/dist/esm/features/jserrors/aggregate/index.js +2 -2
  35. package/dist/esm/features/logging/aggregate/index.js +1 -1
  36. package/dist/esm/features/metrics/aggregate/index.js +2 -1
  37. package/dist/esm/features/page_view_event/aggregate/index.js +1 -1
  38. package/dist/esm/features/session_replay/aggregate/index.js +5 -3
  39. package/dist/esm/features/session_trace/aggregate/index.js +6 -4
  40. package/dist/types/common/config/init.d.ts.map +1 -1
  41. package/dist/types/common/dom/iframe.d.ts +2 -0
  42. package/dist/types/common/dom/iframe.d.ts.map +1 -0
  43. package/dist/types/common/dom/selector-path.d.ts +2 -0
  44. package/dist/types/common/dom/selector-path.d.ts.map +1 -0
  45. package/dist/types/common/event-listener/event-listener-opts.d.ts +2 -2
  46. package/dist/types/common/event-listener/event-listener-opts.d.ts.map +1 -1
  47. package/dist/types/common/timing/time-keeper.d.ts +6 -0
  48. package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
  49. package/dist/types/features/generic_events/aggregate/index.d.ts +16 -1
  50. package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
  51. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts +22 -0
  52. package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts.map +1 -0
  53. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts +12 -0
  54. package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -0
  55. package/dist/types/features/generic_events/constants.d.ts +4 -0
  56. package/dist/types/features/generic_events/constants.d.ts.map +1 -1
  57. package/dist/types/features/generic_events/instrument/index.d.ts.map +1 -1
  58. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  59. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  60. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  61. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  62. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  63. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/common/config/init.js +1 -0
  66. package/src/common/dom/iframe.js +4 -0
  67. package/src/common/dom/selector-path.js +45 -0
  68. package/src/common/event-listener/event-listener-opts.js +5 -30
  69. package/src/common/timing/__mocks__/time-keeper.js +1 -0
  70. package/src/common/timing/time-keeper.js +9 -0
  71. package/src/common/util/stringify.js +1 -1
  72. package/src/features/generic_events/aggregate/index.js +74 -14
  73. package/src/features/generic_events/aggregate/user-actions/aggregated-user-action.js +33 -0
  74. package/src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +73 -0
  75. package/src/features/generic_events/constants.js +6 -0
  76. package/src/features/generic_events/instrument/index.js +19 -3
  77. package/src/features/jserrors/aggregate/index.js +2 -6
  78. package/src/features/logging/aggregate/index.js +1 -3
  79. package/src/features/metrics/aggregate/index.js +2 -1
  80. package/src/features/page_view_event/aggregate/index.js +1 -3
  81. package/src/features/session_replay/aggregate/index.js +5 -7
  82. package/src/features/session_trace/aggregate/index.js +6 -10
@@ -0,0 +1,4 @@
1
+ export function isIFrameWindow(windowObject) {
2
+ if (!windowObject) return false;
3
+ return windowObject.self !== windowObject.top;
4
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Generates a CSS selector path for the given element, if possible
3
+ * @param {HTMLElement} elem
4
+ * @param {boolean} includeId
5
+ * @param {boolean} includeClass
6
+ * @returns {string|undefined}
7
+ */
8
+ export const generateSelectorPath = elem => {
9
+ if (!elem) return;
10
+ const getNthOfTypeIndex = node => {
11
+ try {
12
+ let i = 1;
13
+ const {
14
+ tagName
15
+ } = node;
16
+ while (node.previousElementSibling) {
17
+ if (node.previousElementSibling.tagName === tagName) i++;
18
+ node = node.previousElementSibling;
19
+ }
20
+ return i;
21
+ } catch (err) {
22
+ // do nothing for now. An invalid child count will make the path selector not return a nth-of-type selector statement
23
+ }
24
+ };
25
+ let pathSelector = '';
26
+ let index = getNthOfTypeIndex(elem);
27
+ try {
28
+ while (elem?.tagName) {
29
+ const {
30
+ id,
31
+ localName
32
+ } = elem;
33
+ const selector = [localName, id ? "#".concat(id) : '', pathSelector ? ">".concat(pathSelector) : ''].join('');
34
+ pathSelector = selector;
35
+ elem = elem.parentNode;
36
+ }
37
+ } catch (err) {
38
+ // do nothing for now
39
+ }
40
+ return pathSelector ? index ? "".concat(pathSelector, ":nth-of-type(").concat(index, ")") : pathSelector : undefined;
41
+ };
@@ -1,32 +1,9 @@
1
- import { globalScope } from '../constants/runtime';
2
-
3
- /*
4
- * See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#safely_detecting_option_support
5
- */
6
- let passiveSupported = false;
7
- let signalSupported = false;
8
- try {
9
- const options = {
10
- get passive() {
11
- // this function will be called when the browser attempts to access the passive property
12
- passiveSupported = true;
13
- return false;
14
- },
15
- get signal() {
16
- signalSupported = true;
17
- return false;
18
- }
19
- };
20
- globalScope.addEventListener('test', null, options);
21
- globalScope.removeEventListener('test', null, options);
22
- } catch (err) {}
23
1
  export function eventListenerOpts(useCapture, abortSignal) {
24
- return passiveSupported || signalSupported ? {
25
- capture: !!useCapture,
26
- passive: passiveSupported,
27
- // passive defaults to false
2
+ return {
3
+ capture: useCapture,
4
+ passive: false,
28
5
  signal: abortSignal
29
- } : !!useCapture; // mainly just IE11 doesn't support third param options under EventTarget API
6
+ };
30
7
  }
31
8
 
32
9
  /** Do not use this within the worker context. */
@@ -102,6 +102,15 @@ export class TimeKeeper {
102
102
  return timestamp - this.#localTimeDiff;
103
103
  }
104
104
 
105
+ /**
106
+ * Corrects relative timestamp to NR server time (epoch).
107
+ * @param {DOMHighResTimeStamp} relativeTime
108
+ * @returns {number}
109
+ */
110
+ correctRelativeTimestamp(relativeTime) {
111
+ return this.correctAbsoluteTimestamp(this.convertRelativeTimestamp(relativeTime));
112
+ }
113
+
105
114
  /** Process the session entity and use the info to set the main time calculations if present */
106
115
  processStoredDiff() {
107
116
  if (this.#ready) return; // Time diff has already been calculated
@@ -31,7 +31,7 @@ const getCircularReplacer = () => {
31
31
  */
32
32
  export function stringify(val) {
33
33
  try {
34
- return JSON.stringify(val, getCircularReplacer());
34
+ return JSON.stringify(val, getCircularReplacer()) ?? '';
35
35
  } catch (e) {
36
36
  try {
37
37
  ee.emit('internal-error', [e]);
@@ -6,10 +6,10 @@ 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
8
  import { getInfo } from '../../../common/config/info';
9
- import { getConfigurationValue } from '../../../common/config/init';
9
+ import { getConfiguration } from '../../../common/config/init';
10
10
  import { getRuntime } from '../../../common/config/runtime';
11
11
  import { FEATURE_NAME } from '../constants';
12
- import { isBrowserScope } from '../../../common/constants/runtime';
12
+ import { initialLocation, isBrowserScope } from '../../../common/constants/runtime';
13
13
  import { AggregateBase } from '../../utils/aggregate-base';
14
14
  import { warn } from '../../../common/util/console';
15
15
  import { now } from '../../../common/timing/now';
@@ -19,13 +19,16 @@ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
19
19
  import { EventBuffer } from '../../utils/event-buffer';
20
20
  import { applyFnToProps } from '../../../common/util/traverse';
21
21
  import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants';
22
+ import { UserActionsAggregator } from './user-actions/user-actions-aggregator';
23
+ import { isIFrameWindow } from '../../../common/dom/iframe';
22
24
  export class Aggregate extends AggregateBase {
23
25
  #agentRuntime;
24
26
  static featureName = FEATURE_NAME;
25
27
  constructor(agentIdentifier, aggregator) {
26
28
  super(agentIdentifier, aggregator, FEATURE_NAME);
29
+ const agentInit = getConfiguration(this.agentIdentifier);
27
30
  this.eventsPerHarvest = 1000;
28
- this.harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'generic_events.harvestTimeSeconds');
31
+ this.harvestTimeSeconds = agentInit.generic_events.harvestTimeSeconds;
29
32
  this.referrerUrl = isBrowserScope && document.referrer ? cleanURL(document.referrer) : undefined;
30
33
  this.events = new EventBuffer();
31
34
  this.#agentRuntime = getRuntime(this.agentIdentifier);
@@ -35,16 +38,16 @@ export class Aggregate extends AggregateBase {
35
38
  deregisterDrain(this.agentIdentifier, this.featureName);
36
39
  return;
37
40
  }
38
- if (getConfigurationValue(this.agentIdentifier, 'page_action.enabled')) {
41
+ const preHarvestMethods = [];
42
+ if (agentInit.page_action.enabled) {
39
43
  registerHandler('api-addPageAction', (timestamp, name, attributes) => {
40
44
  this.addEvent({
41
45
  ...attributes,
42
46
  eventType: 'PageAction',
43
- timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(this.#agentRuntime.timeKeeper.convertRelativeTimestamp(timestamp))),
47
+ timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(timestamp)),
44
48
  timeSinceLoad: timestamp / 1000,
45
49
  actionName: name,
46
50
  referrerUrl: this.referrerUrl,
47
- currentUrl: cleanURL('' + location),
48
51
  ...(isBrowserScope && {
49
52
  browserWidth: window.document.documentElement?.clientWidth,
50
53
  browserHeight: window.document.documentElement?.clientHeight
@@ -52,16 +55,83 @@ export class Aggregate extends AggregateBase {
52
55
  });
53
56
  }, this.featureName, this.ee);
54
57
  }
58
+ if (isBrowserScope && agentInit.user_actions.enabled) {
59
+ this.userActionAggregator = new UserActionsAggregator();
60
+ this.addUserAction = aggregatedUserAction => {
61
+ try {
62
+ /** The aggregator process only returns an event when it is "done" aggregating -
63
+ * so we still need to validate that an event was given to this method before we try to add */
64
+ if (aggregatedUserAction?.event) {
65
+ const {
66
+ target,
67
+ timeStamp,
68
+ type
69
+ } = aggregatedUserAction.event;
70
+ this.addEvent({
71
+ eventType: 'UserAction',
72
+ timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(timeStamp)),
73
+ action: type,
74
+ actionCount: aggregatedUserAction.count,
75
+ actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
76
+ actionMs: aggregatedUserAction.relativeMs,
77
+ rageClick: aggregatedUserAction.rageClick,
78
+ target: aggregatedUserAction.selectorPath,
79
+ ...(isIFrameWindow(window) && {
80
+ iframe: true
81
+ }),
82
+ ...(target?.id && {
83
+ targetId: target.id
84
+ }),
85
+ ...(target?.tagName && {
86
+ targetTag: target.tagName
87
+ }),
88
+ ...(target?.type && {
89
+ targetType: target.type
90
+ }),
91
+ ...(target?.className && {
92
+ targetClass: target.className
93
+ })
94
+ });
95
+ }
96
+ } catch (e) {
97
+ // do nothing for now
98
+ }
99
+ };
100
+ registerHandler('ua', evt => {
101
+ /** the processor will return the previously aggregated event if it has been completed by processing the current event */
102
+ this.addUserAction(this.userActionAggregator.process(evt));
103
+ }, this.featureName, this.ee);
104
+ preHarvestMethods.push((options = {}) => {
105
+ /** send whatever UserActions have been aggregated up to this point
106
+ * if we are in a final harvest. By accessing the aggregationEvent, the aggregation is then force-cleared */
107
+ if (options.isFinalHarvest) this.addUserAction(this.userActionAggregator.aggregationEvent);
108
+ });
109
+ }
55
110
  this.harvestScheduler = new HarvestScheduler('ins', {
56
111
  onFinished: (...args) => this.onHarvestFinished(...args)
57
112
  }, this);
58
- this.harvestScheduler.harvest.on('ins', (...args) => this.onHarvestStarted(...args));
113
+ this.harvestScheduler.harvest.on('ins', (...args) => {
114
+ preHarvestMethods.forEach(fn => fn(...args));
115
+ return this.onHarvestStarted(...args);
116
+ });
59
117
  this.harvestScheduler.startTimer(this.harvestTimeSeconds, 0);
60
118
  this.drain();
61
119
  });
62
120
  }
63
121
 
64
122
  // WARNING: Insights times are in seconds. EXCEPT timestamp, which is in ms.
123
+ /** Some keys are set by the query params or request headers sent with the harvest and override the body values, so check those before adding new standard body values...
124
+ * see harvest.js#baseQueryString for more info on the query params
125
+ * Notably:
126
+ * * name: set by the `t=` query param
127
+ * * appId: set by the `a=` query param
128
+ * * standalone: set by the `sa=` query param
129
+ * * session: set by the `s=` query param
130
+ * * sessionTraceId: set by the `ptid=` query param
131
+ * * userAgent*: set by the userAgent header
132
+ * @param {object=} obj the event object for storing in the event buffer
133
+ * @returns void
134
+ */
65
135
  addEvent(obj = {}) {
66
136
  if (!obj || !Object.keys(obj).length) return;
67
137
  if (!obj.eventType) {
@@ -74,9 +144,10 @@ export class Aggregate extends AggregateBase {
74
144
  }
75
145
  const defaultEventAttributes = {
76
146
  /** should be overridden by the event-specific attributes, but just in case -- set it to now() */
77
- timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(this.#agentRuntime.timeKeeper.convertRelativeTimestamp(now()))),
78
- /** all generic events require a pageUrl */
79
- pageUrl: cleanURL(getRuntime(this.agentIdentifier).origin)
147
+ timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(now())),
148
+ /** all generic events require pageUrl(s) */
149
+ pageUrl: cleanURL('' + initialLocation),
150
+ currentUrl: cleanURL('' + location)
80
151
  };
81
152
  const eventAttributes = {
82
153
  /** Agent-level custom attributes */
@@ -104,7 +175,7 @@ export class Aggregate extends AggregateBase {
104
175
  ins: this.events.buffer
105
176
  }, this.obfuscator.obfuscateString.bind(this.obfuscator), 'string')
106
177
  };
107
- if (options.retry) this.events.hold();
178
+ if (options.retry) this.events.hold();else this.events.clear();
108
179
  return payload;
109
180
  }
110
181
  onHarvestFinished(result) {
@@ -0,0 +1,32 @@
1
+ import { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../constants';
2
+ export class AggregatedUserAction {
3
+ constructor(evt, selectorPath) {
4
+ this.event = evt;
5
+ this.count = 1;
6
+ this.originMs = Math.floor(evt.timeStamp);
7
+ this.relativeMs = [0];
8
+ this.selectorPath = selectorPath;
9
+ this.rageClick = undefined;
10
+ }
11
+
12
+ /**
13
+ * Aggregates the count and maintains the relative MS array for matching events
14
+ * Will determine if a rage click was observed as part of the aggregation
15
+ * @param {Event} evt
16
+ * @returns {void}
17
+ */
18
+ aggregate(evt) {
19
+ this.count++;
20
+ this.relativeMs.push(Math.floor(evt.timeStamp - this.originMs));
21
+ if (this.isRageClick()) this.rageClick = true;
22
+ }
23
+
24
+ /**
25
+ * Determines if the current set of relative ms values constitutes a rage click
26
+ * @returns {boolean}
27
+ */
28
+ isRageClick() {
29
+ const len = this.relativeMs.length;
30
+ return this.event.type === 'click' && len >= RAGE_CLICK_THRESHOLD_EVENTS && this.relativeMs[len - 1] - this.relativeMs[len - RAGE_CLICK_THRESHOLD_EVENTS] < RAGE_CLICK_THRESHOLD_MS;
31
+ }
32
+ }
@@ -0,0 +1,70 @@
1
+ import { generateSelectorPath } from '../../../../common/dom/selector-path';
2
+ import { OBSERVED_WINDOW_EVENTS } from '../../constants';
3
+ import { AggregatedUserAction } from './aggregated-user-action';
4
+ export class UserActionsAggregator {
5
+ /** @type {AggregatedUserAction=} */
6
+ #aggregationEvent = undefined;
7
+ #aggregationKey = '';
8
+ get aggregationEvent() {
9
+ // if this is accessed externally, we need to be done aggregating on it
10
+ // to prevent potential mutability and duplication issues, so the state is cleared upon returning.
11
+ // This value may need to be accessed during an unload harvest.
12
+ const finishedEvent = this.#aggregationEvent;
13
+ this.#aggregationKey = '';
14
+ this.#aggregationEvent = undefined;
15
+ return finishedEvent;
16
+ }
17
+
18
+ /**
19
+ * Process the event and determine if a new aggregation set should be made or if it should increment the current aggregation
20
+ * @param {Event} evt The event supplied by the addEventListener callback
21
+ * @returns {AggregatedUserAction|undefined} The previous aggregation set if it has been completed by processing the current event
22
+ */
23
+ process(evt) {
24
+ if (!evt) return;
25
+ const selectorPath = getSelectorPath(evt);
26
+ const aggregationKey = getAggregationKey(evt, selectorPath);
27
+ if (!!aggregationKey && aggregationKey === this.#aggregationKey) {
28
+ // an aggregation exists already, so lets just continue to increment
29
+ this.#aggregationEvent.aggregate(evt);
30
+ } else {
31
+ // return the prev existing one (if there is one)
32
+ const finishedEvent = this.#aggregationEvent;
33
+ // then set as this new event aggregation
34
+ this.#aggregationKey = aggregationKey;
35
+ this.#aggregationEvent = new AggregatedUserAction(evt, selectorPath);
36
+ return finishedEvent;
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Generates a selector path for the event, starting with simple cases like window or document and getting more complex for dom-tree traversals as needed.
43
+ * Will return a random selector path value if no other path can be determined, to force the aggregator to skip aggregation for this event.
44
+ * @param {Event} evt
45
+ * @returns {string}
46
+ */
47
+ function getSelectorPath(evt) {
48
+ let selectorPath;
49
+ if (OBSERVED_WINDOW_EVENTS.includes(evt.type) || evt.target === window) selectorPath = 'window';else if (evt.target === document) selectorPath = 'document';
50
+ // if still no selectorPath, generate one from target tree that includes elem ids
51
+ else selectorPath = generateSelectorPath(evt.target);
52
+ // if STILL no selectorPath, it will return undefined which will skip aggregation for this event
53
+ return selectorPath;
54
+ }
55
+
56
+ /**
57
+ * Returns an aggregation key based on the event type and the selector path of the event's target.
58
+ * Scrollend events are aggregated into one set, no matter what.
59
+ * @param {Event} evt
60
+ * @param {string} selectorPath
61
+ * @returns {string}
62
+ */
63
+ function getAggregationKey(evt, selectorPath) {
64
+ let aggregationKey = evt.type;
65
+ /** aggregate all scrollends into one set (if sequential), no matter what their target is
66
+ * the aggregation group's selector path with be reflected as the first one observed
67
+ * due to the way the aggregation logic works (by storing the initial value and aggregating it) */
68
+ if (evt.type !== 'scrollend') aggregationKey += '-' + selectorPath;
69
+ return aggregationKey;
70
+ }
@@ -1,4 +1,8 @@
1
1
  import { FEATURE_NAMES } from '../../loaders/features/features';
2
2
  export const FEATURE_NAME = FEATURE_NAMES.genericEvents;
3
3
  export const IDEAL_PAYLOAD_SIZE = 64000;
4
- export const MAX_PAYLOAD_SIZE = 1000000;
4
+ export const MAX_PAYLOAD_SIZE = 1000000;
5
+ export const OBSERVED_EVENTS = ['auxclick', 'click', 'copy', 'keydown', 'paste', 'scrollend'];
6
+ export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur'];
7
+ export const RAGE_CLICK_THRESHOLD_EVENTS = 4;
8
+ export const RAGE_CLICK_THRESHOLD_MS = 1000;
@@ -2,17 +2,28 @@
2
2
  * SPDX-License-Identifier: Apache-2.0
3
3
  */
4
4
 
5
- import { getConfigurationValue } from '../../../common/config/init';
5
+ import { getConfiguration } from '../../../common/config/init';
6
+ import { isBrowserScope } from '../../../common/constants/runtime';
6
7
  import { deregisterDrain } from '../../../common/drain/drain';
8
+ import { handle } from '../../../common/event-emitter/handle';
9
+ import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts';
7
10
  import { InstrumentBase } from '../../utils/instrument-base';
8
- import { FEATURE_NAME } from '../constants';
11
+ import { FEATURE_NAME, OBSERVED_EVENTS, OBSERVED_WINDOW_EVENTS } from '../constants';
9
12
  export class Instrument extends InstrumentBase {
10
13
  static featureName = FEATURE_NAME;
11
14
  constructor(agentIdentifier, aggregator, auto = true) {
12
15
  super(agentIdentifier, aggregator, FEATURE_NAME, auto);
13
- const genericEventSourceConfigs = [getConfigurationValue(this.agentIdentifier, 'page_action.enabled')
16
+ const agentInit = getConfiguration(this.agentIdentifier);
17
+ const genericEventSourceConfigs = [agentInit.page_action.enabled, agentInit.user_actions.enabled
14
18
  // other future generic event source configs to go here, like M&Ms, PageResouce, etc.
15
19
  ];
20
+ if (isBrowserScope && agentInit.user_actions.enabled) {
21
+ OBSERVED_EVENTS.forEach(eventType => windowAddEventListener(eventType, evt => handle('ua', [evt], undefined, this.featureName, this.ee), true));
22
+ OBSERVED_WINDOW_EVENTS.forEach(eventType => windowAddEventListener(eventType, evt => handle('ua', [evt], undefined, this.featureName, this.ee))
23
+ // 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.
24
+ );
25
+ }
26
+
16
27
  /** If any of the sources are active, import the aggregator. otherwise deregister */
17
28
  if (genericEventSourceConfigs.some(x => x)) this.importAggregator();else deregisterDrain(this.agentIdentifier, this.featureName);
18
29
  }
@@ -162,7 +162,7 @@ export class Aggregate extends AggregateBase {
162
162
  if (!this.stackReported[bucketHash]) {
163
163
  this.stackReported[bucketHash] = true;
164
164
  params.stack_trace = truncateSize(stackInfo.stackString);
165
- this.observedAt[bucketHash] = Math.floor(agentRuntime.timeKeeper.correctAbsoluteTimestamp(agentRuntime.timeKeeper.convertRelativeTimestamp(time)));
165
+ this.observedAt[bucketHash] = Math.floor(agentRuntime.timeKeeper.correctRelativeTimestamp(time));
166
166
  } else {
167
167
  params.browser_stack_hash = stringHashCode(stackInfo.stackString);
168
168
  }
@@ -178,7 +178,7 @@ export class Aggregate extends AggregateBase {
178
178
  this.pageviewReported[bucketHash] = true;
179
179
  }
180
180
  params.firstOccurrenceTimestamp = this.observedAt[bucketHash];
181
- params.timestamp = Math.floor(agentRuntime.timeKeeper.correctAbsoluteTimestamp(agentRuntime.timeKeeper.convertRelativeTimestamp(time)));
181
+ params.timestamp = Math.floor(agentRuntime.timeKeeper.correctRelativeTimestamp(time));
182
182
  var type = internal ? 'ierr' : 'err';
183
183
  var newMetrics = {
184
184
  time
@@ -60,7 +60,7 @@ export class Aggregate extends AggregateBase {
60
60
  return;
61
61
  }
62
62
  if (typeof message !== 'string' || !message) return warn(32);
63
- const log = new Log(Math.floor(this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(this.#agentRuntime.timeKeeper.convertRelativeTimestamp(timestamp))), message, attributes, level);
63
+ const log = new Log(Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(timestamp)), message, attributes, level);
64
64
  const logBytes = log.message.length + stringify(log.attributes).length + log.level.length + 10; // timestamp == 10 chars
65
65
 
66
66
  if (!this.bufferedLogs.canMerge(logBytes)) {
@@ -10,6 +10,7 @@ import { windowAddEventListener } from '../../../common/event-listener/event-lis
10
10
  import { isBrowserScope, isWorkerScope } from '../../../common/constants/runtime';
11
11
  import { AggregateBase } from '../../utils/aggregate-base';
12
12
  import { deregisterDrain } from '../../../common/drain/drain';
13
+ import { isIFrameWindow } from '../../../common/dom/iframe';
13
14
  // import { WEBSOCKET_TAG } from '../../../common/wrap/wrap-websocket'
14
15
  // import { handleWebsocketEvents } from './websocket-detection'
15
16
 
@@ -105,7 +106,7 @@ export class Aggregate extends AggregateBase {
105
106
  if (proxy.assets) this.storeSupportabilityMetrics('Config/AssetsUrl/Changed');
106
107
  if (proxy.beacon) this.storeSupportabilityMetrics('Config/BeaconUrl/Changed');
107
108
  if (isBrowserScope && window.MutationObserver) {
108
- if (window.self !== window.top) {
109
+ if (isIFrameWindow(window)) {
109
110
  this.storeSupportabilityMetrics('Generic/Runtime/IFrame/Detected');
110
111
  }
111
112
  const preExistingVideos = window.document.querySelectorAll('video').length;
@@ -110,7 +110,7 @@ export class Aggregate extends AggregateBase {
110
110
  queryParameters.fp = firstPaint.current.value;
111
111
  queryParameters.fcp = firstContentfulPaint.current.value;
112
112
  if (this.timeKeeper?.ready) {
113
- queryParameters.timestamp = Math.floor(this.timeKeeper.correctAbsoluteTimestamp(this.timeKeeper.convertRelativeTimestamp(now())));
113
+ queryParameters.timestamp = Math.floor(this.timeKeeper.correctRelativeTimestamp(now()));
114
114
  }
115
115
  const rumStartTime = now();
116
116
  harvester.send({
@@ -28,6 +28,7 @@ import { deregisterDrain } from '../../../common/drain/drain';
28
28
  import { now } from '../../../common/timing/now';
29
29
  import { buildNRMetaNode } from '../shared/utils';
30
30
  import { MAX_PAYLOAD_SIZE } from '../../../common/constants/agent-constants';
31
+ import { cleanURL } from '../../../common/url/clean-url';
31
32
  export class Aggregate extends AggregateBase {
32
33
  static featureName = FEATURE_NAME;
33
34
  mode = MODE.OFF;
@@ -338,8 +339,8 @@ export class Aggregate extends AggregateBase {
338
339
  const firstEventTimestamp = this.getCorrectedTimestamp(events[0]); // from rrweb node
339
340
  const lastEventTimestamp = this.getCorrectedTimestamp(events[events.length - 1]); // from rrweb node
340
341
  // from rrweb node || from when the harvest cycle started
341
- const firstTimestamp = firstEventTimestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp)));
342
- const lastTimestamp = lastEventTimestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(this.timeKeeper.convertRelativeTimestamp(relativeNow)));
342
+ const firstTimestamp = firstEventTimestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp));
343
+ const lastTimestamp = lastEventTimestamp || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow));
343
344
  const agentMetadata = agentRuntime.appMetadata?.agents?.[0] || {};
344
345
  return {
345
346
  qs: {
@@ -377,7 +378,8 @@ export class Aggregate extends AggregateBase {
377
378
  // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
378
379
  ...(endUserId && {
379
380
  'enduser.id': this.obfuscator.obfuscateString(endUserId)
380
- })
381
+ }),
382
+ currentUrl: this.obfuscator.obfuscateString(cleanURL('' + location))
381
383
  // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
382
384
  }, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
383
385
  },
@@ -11,6 +11,7 @@ import { deregisterDrain } from '../../../common/drain/drain';
11
11
  import { globalScope } from '../../../common/constants/runtime';
12
12
  import { MODE, SESSION_EVENTS } from '../../../common/session/constants';
13
13
  import { applyFnToProps } from '../../../common/util/traverse';
14
+ import { cleanURL } from '../../../common/url/clean-url';
14
15
  const ERROR_MODE_SECONDS_WINDOW = 30 * 1000; // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
15
16
  /** Reserved room for query param attrs */
16
17
  const QUERY_PARAM_PADDING = 5000;
@@ -150,7 +151,7 @@ export class Aggregate extends AggregateBase {
150
151
  type: 'BrowserSessionChunk',
151
152
  app_id: this.agentInfo.applicationID,
152
153
  protocol_version: '0',
153
- timestamp: Math.floor(this.timeKeeper.correctAbsoluteTimestamp(this.timeKeeper.convertRelativeTimestamp(earliestTimeStamp))),
154
+ timestamp: Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
154
155
  attributes: encodeObj({
155
156
  ...(agentMetadata.entityGuid && {
156
157
  entityGuid: agentMetadata.entityGuid
@@ -159,8 +160,8 @@ export class Aggregate extends AggregateBase {
159
160
  // this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
160
161
  // if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
161
162
  // trace payload metadata
162
- 'trace.firstTimestamp': Math.floor(this.timeKeeper.correctAbsoluteTimestamp(this.timeKeeper.convertRelativeTimestamp(earliestTimeStamp))),
163
- 'trace.lastTimestamp': Math.floor(this.timeKeeper.correctAbsoluteTimestamp(this.timeKeeper.convertRelativeTimestamp(latestTimeStamp))),
163
+ 'trace.firstTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(earliestTimeStamp)),
164
+ 'trace.lastTimestamp': Math.floor(this.timeKeeper.correctRelativeTimestamp(latestTimeStamp)),
164
165
  'trace.nodes': stns.length,
165
166
  'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
166
167
  // other payload metadata
@@ -176,7 +177,8 @@ export class Aggregate extends AggregateBase {
176
177
  // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
177
178
  ...(endUserId && {
178
179
  'enduser.id': this.obfuscator.obfuscateString(endUserId)
179
- })
180
+ }),
181
+ currentUrl: this.obfuscator.obfuscateString(cleanURL('' + location))
180
182
  // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
181
183
  }, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
182
184
  },
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/common/config/init.js"],"names":[],"mappings":"AAiHA,+CAIC;AAED,0DAKC;AAED,+DAYC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/common/config/init.js"],"names":[],"mappings":"AAkHA,+CAIC;AAED,0DAKC;AAED,+DAYC"}
@@ -0,0 +1,2 @@
1
+ export function isIFrameWindow(windowObject: any): boolean;
2
+ //# sourceMappingURL=iframe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iframe.d.ts","sourceRoot":"","sources":["../../../../src/common/dom/iframe.js"],"names":[],"mappings":"AAAA,2DAGC"}
@@ -0,0 +1,2 @@
1
+ export function generateSelectorPath(elem: HTMLElement): string | undefined;
2
+ //# sourceMappingURL=selector-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selector-path.d.ts","sourceRoot":"","sources":["../../../../src/common/dom/selector-path.js"],"names":[],"mappings":"AAOO,2CALI,WAAW,GAGT,MAAM,GAAC,SAAS,CAuC5B"}
@@ -1,5 +1,5 @@
1
- export function eventListenerOpts(useCapture: any, abortSignal: any): boolean | {
2
- capture: boolean;
1
+ export function eventListenerOpts(useCapture: any, abortSignal: any): {
2
+ capture: any;
3
3
  passive: boolean;
4
4
  signal: any;
5
5
  };
@@ -1 +1 @@
1
- {"version":3,"file":"event-listener-opts.d.ts","sourceRoot":"","sources":["../../../../src/common/event-listener/event-listener-opts.js"],"names":[],"mappings":"AAuBA;;;;EAQC;AAED,iDAAiD;AACjD,wHAEC;AACD,iDAAiD;AACjD,0HAEC"}
1
+ {"version":3,"file":"event-listener-opts.d.ts","sourceRoot":"","sources":["../../../../src/common/event-listener/event-listener-opts.js"],"names":[],"mappings":"AAAA;;;;EAMC;AAED,iDAAiD;AACjD,wHAEC;AACD,iDAAiD;AACjD,0HAEC"}
@@ -36,6 +36,12 @@ export class TimeKeeper {
36
36
  * @return {number} Corrected unix/epoch timestamp
37
37
  */
38
38
  correctAbsoluteTimestamp(timestamp: number): number;
39
+ /**
40
+ * Corrects relative timestamp to NR server time (epoch).
41
+ * @param {DOMHighResTimeStamp} relativeTime
42
+ * @returns {number}
43
+ */
44
+ correctRelativeTimestamp(relativeTime: DOMHighResTimeStamp): number;
39
45
  /** Process the session entity and use the info to set the main time calculations if present */
40
46
  processStoredDiff(): void;
41
47
  #private;
@@ -1 +1 @@
1
- {"version":3,"file":"time-keeper.d.ts","sourceRoot":"","sources":["../../../../src/common/timing/time-keeper.js"],"names":[],"mappings":"AAGA;;;;GAIG;AACH;IA2BE,kCAGC;IAED,qBAEC;IAED,kCAEC;IAED,4BAEC;IAED;;;;;;OAMG;IACH,8BALsB,cAAc,aACf,MAAM,WACR,MAAM,gBACD,MAAM,QAqB7B;IAED;;;;;OAKG;IACH,uCAHwB,MAAM,GACjB,MAAM,CAIlB;IAED;;;;;OAKG;IACH,0CAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,oCAHqB,MAAM,GACf,MAAM,CAIjB;IAED,+FAA+F;IAC/F,0BASC;;CACF"}
1
+ {"version":3,"file":"time-keeper.d.ts","sourceRoot":"","sources":["../../../../src/common/timing/time-keeper.js"],"names":[],"mappings":"AAGA;;;;GAIG;AACH;IA2BE,kCAGC;IAED,qBAEC;IAED,kCAEC;IAED,4BAEC;IAED;;;;;;OAMG;IACH,8BALsB,cAAc,aACf,MAAM,WACR,MAAM,gBACD,MAAM,QAqB7B;IAED;;;;;OAKG;IACH,uCAHwB,MAAM,GACjB,MAAM,CAIlB;IAED;;;;;OAKG;IACH,0CAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,oCAHqB,MAAM,GACf,MAAM,CAIjB;IAED;;;;OAIG;IACH,uCAHW,mBAAmB,GACjB,MAAM,CAIlB;IAED,+FAA+F;IAC/F,0BASC;;CACF"}
@@ -5,8 +5,22 @@ export class Aggregate extends AggregateBase {
5
5
  harvestTimeSeconds: any;
6
6
  referrerUrl: string | undefined;
7
7
  events: EventBuffer;
8
+ userActionAggregator: UserActionsAggregator;
9
+ addUserAction: (aggregatedUserAction: any) => void;
8
10
  harvestScheduler: HarvestScheduler;
9
- addEvent(obj?: {}): void;
11
+ /** Some keys are set by the query params or request headers sent with the harvest and override the body values, so check those before adding new standard body values...
12
+ * see harvest.js#baseQueryString for more info on the query params
13
+ * Notably:
14
+ * * name: set by the `t=` query param
15
+ * * appId: set by the `a=` query param
16
+ * * standalone: set by the `sa=` query param
17
+ * * session: set by the `s=` query param
18
+ * * sessionTraceId: set by the `ptid=` query param
19
+ * * userAgent*: set by the userAgent header
20
+ * @param {object=} obj the event object for storing in the event buffer
21
+ * @returns void
22
+ */
23
+ addEvent(obj?: object | undefined): void;
10
24
  onHarvestStarted(options: any): {
11
25
  qs: {
12
26
  ua: any;
@@ -20,5 +34,6 @@ export class Aggregate extends AggregateBase {
20
34
  }
21
35
  import { AggregateBase } from '../../utils/aggregate-base';
22
36
  import { EventBuffer } from '../../utils/event-buffer';
37
+ import { UserActionsAggregator } from './user-actions/user-actions-aggregator';
23
38
  import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler';
24
39
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAsBA;IAEE,2BAAiC;IACjC,mDA6CC;IA1CC,yBAA4B;IAC5B,wBAA0G;IAE1G,gCAAkG;IAElG,oBAA+B;IA+B7B,mCAAuH;IAS3H,yBAiCC;IAED;;;;;;kBAgBC;IAED,qCAGC;IAED,yBAMC;;CACF;8BA9H6B,4BAA4B;4BAM9B,0BAA0B;iCAbrB,2CAA2C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAwBA;IAEE,2BAAiC;IACjC,mDA0FC;IAtFC,yBAA4B;IAC5B,wBAAqE;IAErE,gCAAkG;IAElG,oBAA+B;IA+B3B,4CAAuD;IAEvD,mDAyBC;IAcH,mCAAuH;IAY3H;;;;;;;;;;;OAWG;IACH,eAHW,MAAM,YAAC,QAmCjB;IAED;;;;;;kBAkBC;IAED,qCAGC;IAED,yBAMC;;CACF;8BA1L6B,4BAA4B;4BAM9B,0BAA0B;sCAGhB,wCAAwC;iCAhB7C,2CAA2C"}