@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,22 @@
1
+ export class AggregatedUserAction {
2
+ constructor(evt: any, selectorPath: any);
3
+ event: any;
4
+ count: number;
5
+ originMs: number;
6
+ relativeMs: number[];
7
+ selectorPath: any;
8
+ rageClick: boolean | undefined;
9
+ /**
10
+ * Aggregates the count and maintains the relative MS array for matching events
11
+ * Will determine if a rage click was observed as part of the aggregation
12
+ * @param {Event} evt
13
+ * @returns {void}
14
+ */
15
+ aggregate(evt: Event): void;
16
+ /**
17
+ * Determines if the current set of relative ms values constitutes a rage click
18
+ * @returns {boolean}
19
+ */
20
+ isRageClick(): boolean;
21
+ }
22
+ //# sourceMappingURL=aggregated-user-action.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregated-user-action.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/aggregated-user-action.js"],"names":[],"mappings":"AAEA;IACE,yCAOC;IANC,WAAgB;IAChB,cAAc;IACd,iBAAyC;IACzC,qBAAqB;IACrB,kBAAgC;IAChC,+BAA0B;IAG5B;;;;;OAKG;IACH,eAHW,KAAK,GACH,IAAI,CAMhB;IAED;;;OAGG;IACH,eAFa,OAAO,CAKnB;CACF"}
@@ -0,0 +1,12 @@
1
+ export class UserActionsAggregator {
2
+ get aggregationEvent(): AggregatedUserAction | undefined;
3
+ /**
4
+ * Process the event and determine if a new aggregation set should be made or if it should increment the current aggregation
5
+ * @param {Event} evt The event supplied by the addEventListener callback
6
+ * @returns {AggregatedUserAction|undefined} The previous aggregation set if it has been completed by processing the current event
7
+ */
8
+ process(evt: Event): AggregatedUserAction | undefined;
9
+ #private;
10
+ }
11
+ import { AggregatedUserAction } from './aggregated-user-action';
12
+ //# sourceMappingURL=user-actions-aggregator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-actions-aggregator.d.ts","sourceRoot":"","sources":["../../../../../../src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js"],"names":[],"mappings":"AAIA;IAKE,yDAQC;IAED;;;;OAIG;IACH,aAHW,KAAK,GACH,oBAAoB,GAAC,SAAS,CAiB1C;;CACF;qCAtCoC,0BAA0B"}
@@ -1,4 +1,8 @@
1
1
  export const FEATURE_NAME: string;
2
2
  export const IDEAL_PAYLOAD_SIZE: 64000;
3
3
  export const MAX_PAYLOAD_SIZE: 1000000;
4
+ export const OBSERVED_EVENTS: string[];
5
+ export const OBSERVED_WINDOW_EVENTS: string[];
6
+ export const RAGE_CLICK_THRESHOLD_EVENTS: 4;
7
+ export const RAGE_CLICK_THRESHOLD_MS: 1000;
4
8
  //# sourceMappingURL=constants.d.ts.map
@@ -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"}
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 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/instrument/index.js"],"names":[],"mappings":"AASA;IACE,2BAAiC;IACjC,mEASC;CACF;AAED,8CAAuC;+BAjBR,6BAA6B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/instrument/index.js"],"names":[],"mappings":"AAYA;IACE,2BAAiC;IACjC,mEAsBC;CACF;AAED,8CAAuC;+BA9BR,6BAA6B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/jserrors/aggregate/index.js"],"names":[],"mappings":"AA2BA;;GAEG;AAEH;IACE,2BAAiC;IACjC,mDAgCC;IA7BC,kBAAuB;IACvB,eAAoB;IACpB,qBAA0B;IAC1B,2BAAgC;IAChC,uCAA4B;IAC5B,qBAAwB;IA0B1B;;;MA2BC;IAED,qCAWC;IAED,8BAEC;IAED,oEAMC;IAED;;;;;;OAMG;IACH,qCAHW,SAAS,GACP,MAAM,CAgBlB;IAED,4FAgGC;IA4BD,yDA6BC;IAED,qFAOC;;CAwBF;wBA9SY,OAAO,0BAA0B,EAAE,SAAS;8BAP3B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/jserrors/aggregate/index.js"],"names":[],"mappings":"AA2BA;;GAEG;AAEH;IACE,2BAAiC;IACjC,mDAgCC;IA7BC,kBAAuB;IACvB,eAAoB;IACpB,qBAA0B;IAC1B,2BAAgC;IAChC,uCAA4B;IAC5B,qBAAwB;IA0B1B;;;MA2BC;IAED,qCAWC;IAED,8BAEC;IAED,oEAMC;IAED;;;;;;OAMG;IACH,qCAHW,SAAS,GACP,MAAM,CAgBlB;IAED,4FA4FC;IA4BD,yDA6BC;IAED,qFAOC;;CAwBF;wBA1SY,OAAO,0BAA0B,EAAE,SAAS;8BAP3B,4BAA4B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/logging/aggregate/index.js"],"names":[],"mappings":"AAiBA;IACE,2BAAiC;IAGjC,mDAwBC;IArBC,+BAA+B;IAC/B,0BAAqC;IAKrC,wBAAmG;IAGjG,4BAKQ;IASZ,+EA+CC;IAED;;;;;;gBASQ,0FAA0F;;;;;;;;;;;;YAY5F,0DAA0D;;;kBAY/D;IAED,qCAGC;;CACF;8BA9H6B,4BAA4B;4BAM9B,0BAA0B;iCAVrB,2CAA2C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/logging/aggregate/index.js"],"names":[],"mappings":"AAiBA;IACE,2BAAiC;IAGjC,mDAwBC;IArBC,+BAA+B;IAC/B,0BAAqC;IAKrC,wBAAmG;IAGjG,4BAKQ;IASZ,+EA6CC;IAED;;;;;;gBASQ,0FAA0F;;;;;;;;;;;;YAY5F,0DAA0D;;;kBAY/D;IAED,qCAGC;;CACF;8BA5H6B,4BAA4B;4BAM9B,0BAA0B;iCAVrB,2CAA2C"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/metrics/aggregate/index.js"],"names":[],"mappings":"AAeA;IACE,2BAAiC;IACjC,mDAsBC;IAED,wDAKC;IAED,iDAKC;IAED,qBAqEC;IAED,0BAOC;IAED,eAkCC;IA/BG,mCAAyB;CAgC9B;8BAhK6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/metrics/aggregate/index.js"],"names":[],"mappings":"AAgBA;IACE,2BAAiC;IACjC,mDAsBC;IAED,wDAKC;IAED,iDAKC;IAED,qBAqEC;IAED,0BAOC;IAED,eAkCC;IA/BG,mCAAyB;CAgC9B;8BAjK6B,4BAA4B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_event/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAA2C;IAC3C,mDA0BC;IAvBC,wBAAwB;IACxB,8BAA8B;IAC9B,8BAA8B;IAC9B,uBAAsD;IAsBxD,gBAmGC;CACF;8BA1I6B,4BAA4B;2BAK/B,oCAAoC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_event/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAA2C;IAC3C,mDA0BC;IAvBC,wBAAwB;IACxB,8BAA8B;IAC9B,8BAA8B;IAC9B,uBAAsD;IAsBxD,gBAiGC;CACF;8BAxI6B,4BAA4B;2BAK/B,oCAAoC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AA+BA;IACE,2BAAiC;IAIjC,8DAsGC;IAzGD,aAAe;IAKb,8GAA8G;IAC9G,wBAAgH;IAChH,iFAAiF;IACjF,qBAAwB;IAGxB,2CAA2C;IAC3C,sDAAwB;IACxB,6CAA6C;IAC7C,gDAAmB;IAEnB,0BAA0B;IAC1B,kBAAqB;IACrB,6CAA6C;IAC7C,gBAA2B;IAE3B,cAA8B;IAC9B,kBAA+C;IAoC/C,4BAKQ;IA4CV,0BAMC;IAED,qBAWC;IAED;;;;;;;OAOG;IACH,gDAHW,OAAO,GACL,IAAI,CA8DhB;IAED,2BASC;IAED;;;;;;;;;;;;oBAiDC;IAED,sCAIC;IAED;;;;;;;;;;MA2EC;IAED,qCAOC;IAED;;;;OAIG;IACH,mCAKC;IAED,yDAAyD;IACzD,+CAUC;IAED,yCAGC;CACF;8BAhZ6B,4BAA4B;iCALzB,2CAA2C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AAgCA;IACE,2BAAiC;IAIjC,8DAsGC;IAzGD,aAAe;IAKb,8GAA8G;IAC9G,wBAAgH;IAChH,iFAAiF;IACjF,qBAAwB;IAGxB,2CAA2C;IAC3C,sDAAwB;IACxB,6CAA6C;IAC7C,gDAAmB;IAEnB,0BAA0B;IAC1B,kBAAqB;IACrB,6CAA6C;IAC7C,gBAA2B;IAE3B,cAA8B;IAC9B,kBAA+C;IAoC/C,4BAKQ;IA4CV,0BAMC;IAED,qBAWC;IAED;;;;;;;OAOG;IACH,gDAHW,OAAO,GACL,IAAI,CA8DhB;IAED,2BASC;IAED;;;;;;;;;;;;oBAiDC;IAED,sCAIC;IAED;;;;;;;;;;MAwEC;IAED,qCAOC;IAED;;;;OAIG;IACH,mCAKC;IAED,yDAAyD;IACzD,+CAUC;IAED,yCAGC;CACF;8BA9Y6B,4BAA4B;iCALzB,2CAA2C"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_trace/aggregate/index.js"],"names":[],"mappings":"AAiBA;IACE,2BAAiC;IAEjC,mDAmBC;IAjBC,kBAA+C;IAC/C,eAAyC;IAEzC,0FAA0F;IAC1F,wBAAqB;IACrB,wBAA0G;IAC1G,0GAA0G;IAC1G,cAAyB;IACzB,mIAAmI;IACnI,uBAA0B;IAC1B,0CAA0C;IAC1C,oBAAuB;IACvB,wIAAwI;IACxI,2BAA0C;IAM5C,gLAAgL;IAChL,mEAoEC;IA/DG,iCAAuB;IACvB,yJAAyJ;IACzJ,UAAkC;IAClC,eAAuD;IAmBD,UAA4D;IASpH,wCAKQ;IA6BV,kJAAkJ;IAClJ,wBAIC;IAED,iJAAiJ;IACjJ;;;;;;;;;;MAkEC;IAED;;OAEG;IACH,qCAKC;IAED,8DAA8D;IAC9D,qBAUC;IAED,2DAA2D;IAC3D,yBAKC;CACF;8BApN6B,4BAA4B;6BAC7B,iBAAiB;iCANb,2CAA2C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_trace/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAAiC;IAEjC,mDAmBC;IAjBC,kBAA+C;IAC/C,eAAyC;IAEzC,0FAA0F;IAC1F,wBAAqB;IACrB,wBAA0G;IAC1G,0GAA0G;IAC1G,cAAyB;IACzB,mIAAmI;IACnI,uBAA0B;IAC1B,0CAA0C;IAC1C,oBAAuB;IACvB,wIAAwI;IACxI,2BAA0C;IAM5C,gLAAgL;IAChL,mEAoEC;IA/DG,iCAAuB;IACvB,yJAAyJ;IACzJ,UAAkC;IAClC,eAAuD;IAmBD,UAA4D;IASpH,wCAKQ;IA6BV,kJAAkJ;IAClJ,wBAIC;IAED,iJAAiJ;IACjJ;;;;;;;;;;MA6DC;IAED;;OAEG;IACH,qCAKC;IAED,8DAA8D;IAC9D,qBAUC;IAED,2DAA2D;IAC3D,yBAKC;CACF;8BAhN6B,4BAA4B;6BAC7B,iBAAiB;iCANb,2CAA2C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.267.0",
3
+ "version": "1.268.0",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -48,6 +48,7 @@ const model = () => {
48
48
  metrics: { enabled: true, autoStart: true },
49
49
  obfuscate: undefined,
50
50
  page_action: { enabled: true },
51
+ user_actions: { enabled: true },
51
52
  page_view_event: { enabled: true, autoStart: true },
52
53
  page_view_timing: { enabled: true, harvestTimeSeconds: 30, autoStart: true },
53
54
  privacy: { cookies_enabled: true }, // *cli - per discussion, default should be true
@@ -0,0 +1,4 @@
1
+ export function isIFrameWindow (windowObject) {
2
+ if (!windowObject) return false
3
+ return windowObject.self !== windowObject.top
4
+ }
@@ -0,0 +1,45 @@
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
+
11
+ const getNthOfTypeIndex = (node) => {
12
+ try {
13
+ let i = 1
14
+ const { tagName } = node
15
+ while (node.previousElementSibling) {
16
+ if (node.previousElementSibling.tagName === tagName) i++
17
+ node = node.previousElementSibling
18
+ }
19
+ return i
20
+ } catch (err) {
21
+ // do nothing for now. An invalid child count will make the path selector not return a nth-of-type selector statement
22
+ }
23
+ }
24
+
25
+ let pathSelector = ''
26
+ let index = getNthOfTypeIndex(elem)
27
+
28
+ try {
29
+ while (elem?.tagName) {
30
+ const { id, localName } = elem
31
+ const selector = [
32
+ localName,
33
+ id ? `#${id}` : '',
34
+ pathSelector ? `>${pathSelector}` : ''
35
+ ].join('')
36
+
37
+ pathSelector = selector
38
+ elem = elem.parentNode
39
+ }
40
+ } catch (err) {
41
+ // do nothing for now
42
+ }
43
+
44
+ return pathSelector ? index ? `${pathSelector}:nth-of-type(${index})` : pathSelector : undefined
45
+ }
@@ -1,34 +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 () { // this function will be called when the browser attempts to access the passive property
11
- passiveSupported = true
12
- return false
13
- },
14
- get signal () {
15
- signalSupported = true
16
- return false
17
- }
18
- }
19
-
20
- globalScope.addEventListener('test', null, options)
21
- globalScope.removeEventListener('test', null, options)
22
- } catch (err) {}
23
-
24
1
  export function eventListenerOpts (useCapture, abortSignal) {
25
- return (passiveSupported || signalSupported)
26
- ? {
27
- capture: !!useCapture,
28
- passive: passiveSupported, // passive defaults to false
29
- signal: abortSignal
30
- }
31
- : !!useCapture // mainly just IE11 doesn't support third param options under EventTarget API
2
+ return {
3
+ capture: useCapture,
4
+ passive: false,
5
+ signal: abortSignal
6
+ }
32
7
  }
33
8
 
34
9
  /** Do not use this within the worker context. */
@@ -3,4 +3,5 @@ export const TimeKeeper = jest.fn(function () {
3
3
  this.convertRelativeTimestamp = jest.fn()
4
4
  this.convertAbsoluteTimestamp = jest.fn()
5
5
  this.correctAbsoluteTimestamp = jest.fn()
6
+ this.correctRelativeTimestamp = jest.fn()
6
7
  })
@@ -107,6 +107,15 @@ export class TimeKeeper {
107
107
  return timestamp - this.#localTimeDiff
108
108
  }
109
109
 
110
+ /**
111
+ * Corrects relative timestamp to NR server time (epoch).
112
+ * @param {DOMHighResTimeStamp} relativeTime
113
+ * @returns {number}
114
+ */
115
+ correctRelativeTimestamp (relativeTime) {
116
+ return this.correctAbsoluteTimestamp(this.convertRelativeTimestamp(relativeTime))
117
+ }
118
+
110
119
  /** Process the session entity and use the info to set the main time calculations if present */
111
120
  processStoredDiff () {
112
121
  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,15 +19,18 @@ 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
 
23
25
  export class Aggregate extends AggregateBase {
24
26
  #agentRuntime
25
27
  static featureName = FEATURE_NAME
26
28
  constructor (agentIdentifier, aggregator) {
27
29
  super(agentIdentifier, aggregator, FEATURE_NAME)
30
+ const agentInit = getConfiguration(this.agentIdentifier)
28
31
 
29
32
  this.eventsPerHarvest = 1000
30
- this.harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'generic_events.harvestTimeSeconds')
33
+ this.harvestTimeSeconds = agentInit.generic_events.harvestTimeSeconds
31
34
 
32
35
  this.referrerUrl = (isBrowserScope && document.referrer) ? cleanURL(document.referrer) : undefined
33
36
 
@@ -42,18 +45,17 @@ export class Aggregate extends AggregateBase {
42
45
  return
43
46
  }
44
47
 
45
- if (getConfigurationValue(this.agentIdentifier, 'page_action.enabled')) {
48
+ const preHarvestMethods = []
49
+
50
+ if (agentInit.page_action.enabled) {
46
51
  registerHandler('api-addPageAction', (timestamp, name, attributes) => {
47
52
  this.addEvent({
48
53
  ...attributes,
49
54
  eventType: 'PageAction',
50
- timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(
51
- this.#agentRuntime.timeKeeper.convertRelativeTimestamp(timestamp)
52
- )),
55
+ timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(timestamp)),
53
56
  timeSinceLoad: timestamp / 1000,
54
57
  actionName: name,
55
58
  referrerUrl: this.referrerUrl,
56
- currentUrl: cleanURL('' + location),
57
59
  ...(isBrowserScope && {
58
60
  browserWidth: window.document.documentElement?.clientWidth,
59
61
  browserHeight: window.document.documentElement?.clientHeight
@@ -62,8 +64,53 @@ export class Aggregate extends AggregateBase {
62
64
  }, this.featureName, this.ee)
63
65
  }
64
66
 
67
+ if (isBrowserScope && agentInit.user_actions.enabled) {
68
+ this.userActionAggregator = new UserActionsAggregator()
69
+
70
+ this.addUserAction = (aggregatedUserAction) => {
71
+ try {
72
+ /** The aggregator process only returns an event when it is "done" aggregating -
73
+ * so we still need to validate that an event was given to this method before we try to add */
74
+ if (aggregatedUserAction?.event) {
75
+ const { target, timeStamp, type } = aggregatedUserAction.event
76
+ this.addEvent({
77
+ eventType: 'UserAction',
78
+ timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(timeStamp)),
79
+ action: type,
80
+ actionCount: aggregatedUserAction.count,
81
+ actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
82
+ actionMs: aggregatedUserAction.relativeMs,
83
+ rageClick: aggregatedUserAction.rageClick,
84
+ target: aggregatedUserAction.selectorPath,
85
+ ...(isIFrameWindow(window) && { iframe: true }),
86
+ ...(target?.id && { targetId: target.id }),
87
+ ...(target?.tagName && { targetTag: target.tagName }),
88
+ ...(target?.type && { targetType: target.type }),
89
+ ...(target?.className && { targetClass: target.className })
90
+ })
91
+ }
92
+ } catch (e) {
93
+ // do nothing for now
94
+ }
95
+ }
96
+
97
+ registerHandler('ua', (evt) => {
98
+ /** the processor will return the previously aggregated event if it has been completed by processing the current event */
99
+ this.addUserAction(this.userActionAggregator.process(evt))
100
+ }, this.featureName, this.ee)
101
+
102
+ preHarvestMethods.push((options = {}) => {
103
+ /** send whatever UserActions have been aggregated up to this point
104
+ * if we are in a final harvest. By accessing the aggregationEvent, the aggregation is then force-cleared */
105
+ if (options.isFinalHarvest) this.addUserAction(this.userActionAggregator.aggregationEvent)
106
+ })
107
+ }
108
+
65
109
  this.harvestScheduler = new HarvestScheduler('ins', { onFinished: (...args) => this.onHarvestFinished(...args) }, this)
66
- this.harvestScheduler.harvest.on('ins', (...args) => this.onHarvestStarted(...args))
110
+ this.harvestScheduler.harvest.on('ins', (...args) => {
111
+ preHarvestMethods.forEach(fn => fn(...args))
112
+ return this.onHarvestStarted(...args)
113
+ })
67
114
  this.harvestScheduler.startTimer(this.harvestTimeSeconds, 0)
68
115
 
69
116
  this.drain()
@@ -71,6 +118,18 @@ export class Aggregate extends AggregateBase {
71
118
  }
72
119
 
73
120
  // WARNING: Insights times are in seconds. EXCEPT timestamp, which is in ms.
121
+ /** 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...
122
+ * see harvest.js#baseQueryString for more info on the query params
123
+ * Notably:
124
+ * * name: set by the `t=` query param
125
+ * * appId: set by the `a=` query param
126
+ * * standalone: set by the `sa=` query param
127
+ * * session: set by the `s=` query param
128
+ * * sessionTraceId: set by the `ptid=` query param
129
+ * * userAgent*: set by the userAgent header
130
+ * @param {object=} obj the event object for storing in the event buffer
131
+ * @returns void
132
+ */
74
133
  addEvent (obj = {}) {
75
134
  if (!obj || !Object.keys(obj).length) return
76
135
  if (!obj.eventType) {
@@ -85,11 +144,10 @@ export class Aggregate extends AggregateBase {
85
144
 
86
145
  const defaultEventAttributes = {
87
146
  /** should be overridden by the event-specific attributes, but just in case -- set it to now() */
88
- timestamp: Math.floor(this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(
89
- this.#agentRuntime.timeKeeper.convertRelativeTimestamp(now())
90
- )),
91
- /** all generic events require a pageUrl */
92
- 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)
93
151
  }
94
152
 
95
153
  const eventAttributes = {
@@ -121,6 +179,8 @@ export class Aggregate extends AggregateBase {
121
179
  })
122
180
 
123
181
  if (options.retry) this.events.hold()
182
+ else this.events.clear()
183
+
124
184
  return payload
125
185
  }
126
186
 
@@ -0,0 +1,33 @@
1
+ import { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../constants'
2
+
3
+ export class AggregatedUserAction {
4
+ constructor (evt, selectorPath) {
5
+ this.event = evt
6
+ this.count = 1
7
+ this.originMs = Math.floor(evt.timeStamp)
8
+ this.relativeMs = [0]
9
+ this.selectorPath = selectorPath
10
+ this.rageClick = undefined
11
+ }
12
+
13
+ /**
14
+ * Aggregates the count and maintains the relative MS array for matching events
15
+ * Will determine if a rage click was observed as part of the aggregation
16
+ * @param {Event} evt
17
+ * @returns {void}
18
+ */
19
+ aggregate (evt) {
20
+ this.count++
21
+ this.relativeMs.push(Math.floor(evt.timeStamp - this.originMs))
22
+ if (this.isRageClick()) this.rageClick = true
23
+ }
24
+
25
+ /**
26
+ * Determines if the current set of relative ms values constitutes a rage click
27
+ * @returns {boolean}
28
+ */
29
+ isRageClick () {
30
+ const len = this.relativeMs.length
31
+ 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)
32
+ }
33
+ }
@@ -0,0 +1,73 @@
1
+ import { generateSelectorPath } from '../../../../common/dom/selector-path'
2
+ import { OBSERVED_WINDOW_EVENTS } from '../../constants'
3
+ import { AggregatedUserAction } from './aggregated-user-action'
4
+
5
+ export class UserActionsAggregator {
6
+ /** @type {AggregatedUserAction=} */
7
+ #aggregationEvent = undefined
8
+ #aggregationKey = ''
9
+
10
+ get aggregationEvent () {
11
+ // if this is accessed externally, we need to be done aggregating on it
12
+ // to prevent potential mutability and duplication issues, so the state is cleared upon returning.
13
+ // This value may need to be accessed during an unload harvest.
14
+ const finishedEvent = this.#aggregationEvent
15
+ this.#aggregationKey = ''
16
+ this.#aggregationEvent = undefined
17
+ return finishedEvent
18
+ }
19
+
20
+ /**
21
+ * Process the event and determine if a new aggregation set should be made or if it should increment the current aggregation
22
+ * @param {Event} evt The event supplied by the addEventListener callback
23
+ * @returns {AggregatedUserAction|undefined} The previous aggregation set if it has been completed by processing the current event
24
+ */
25
+ process (evt) {
26
+ if (!evt) return
27
+ const selectorPath = getSelectorPath(evt)
28
+ const aggregationKey = getAggregationKey(evt, selectorPath)
29
+ if (!!aggregationKey && aggregationKey === this.#aggregationKey) {
30
+ // an aggregation exists already, so lets just continue to increment
31
+ this.#aggregationEvent.aggregate(evt)
32
+ } else {
33
+ // return the prev existing one (if there is one)
34
+ const finishedEvent = this.#aggregationEvent
35
+ // then set as this new event aggregation
36
+ this.#aggregationKey = aggregationKey
37
+ this.#aggregationEvent = new AggregatedUserAction(evt, selectorPath)
38
+ return finishedEvent
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * 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.
45
+ * Will return a random selector path value if no other path can be determined, to force the aggregator to skip aggregation for this event.
46
+ * @param {Event} evt
47
+ * @returns {string}
48
+ */
49
+ function getSelectorPath (evt) {
50
+ let selectorPath
51
+ if (OBSERVED_WINDOW_EVENTS.includes(evt.type) || evt.target === window) selectorPath = 'window'
52
+ else if (evt.target === document) selectorPath = 'document'
53
+ // if still no selectorPath, generate one from target tree that includes elem ids
54
+ else selectorPath = generateSelectorPath(evt.target)
55
+ // if STILL no selectorPath, it will return undefined which will skip aggregation for this event
56
+ return selectorPath
57
+ }
58
+
59
+ /**
60
+ * Returns an aggregation key based on the event type and the selector path of the event's target.
61
+ * Scrollend events are aggregated into one set, no matter what.
62
+ * @param {Event} evt
63
+ * @param {string} selectorPath
64
+ * @returns {string}
65
+ */
66
+ function getAggregationKey (evt, selectorPath) {
67
+ let aggregationKey = evt.type
68
+ /** aggregate all scrollends into one set (if sequential), no matter what their target is
69
+ * the aggregation group's selector path with be reflected as the first one observed
70
+ * due to the way the aggregation logic works (by storing the initial value and aggregating it) */
71
+ if (evt.type !== 'scrollend') aggregationKey += '-' + selectorPath
72
+ return aggregationKey
73
+ }
@@ -3,3 +3,9 @@ import { FEATURE_NAMES } from '../../loaders/features/features'
3
3
  export const FEATURE_NAME = FEATURE_NAMES.genericEvents
4
4
  export const IDEAL_PAYLOAD_SIZE = 64000
5
5
  export const MAX_PAYLOAD_SIZE = 1000000
6
+
7
+ export const OBSERVED_EVENTS = ['auxclick', 'click', 'copy', 'keydown', 'paste', 'scrollend']
8
+ export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur']
9
+
10
+ export const RAGE_CLICK_THRESHOLD_EVENTS = 4
11
+ export const RAGE_CLICK_THRESHOLD_MS = 1000
@@ -2,19 +2,35 @@
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
 
10
13
  export class Instrument extends InstrumentBase {
11
14
  static featureName = FEATURE_NAME
12
15
  constructor (agentIdentifier, aggregator, auto = true) {
13
16
  super(agentIdentifier, aggregator, FEATURE_NAME, auto)
17
+ const agentInit = getConfiguration(this.agentIdentifier)
14
18
  const genericEventSourceConfigs = [
15
- getConfigurationValue(this.agentIdentifier, 'page_action.enabled')
19
+ agentInit.page_action.enabled,
20
+ agentInit.user_actions.enabled
16
21
  // other future generic event source configs to go here, like M&Ms, PageResouce, etc.
17
22
  ]
23
+
24
+ if (isBrowserScope && agentInit.user_actions.enabled) {
25
+ OBSERVED_EVENTS.forEach(eventType =>
26
+ windowAddEventListener(eventType, (evt) => handle('ua', [evt], undefined, this.featureName, this.ee), true)
27
+ )
28
+ OBSERVED_WINDOW_EVENTS.forEach(eventType =>
29
+ windowAddEventListener(eventType, (evt) => handle('ua', [evt], undefined, this.featureName, this.ee))
30
+ // 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.
31
+ )
32
+ }
33
+
18
34
  /** If any of the sources are active, import the aggregator. otherwise deregister */
19
35
  if (genericEventSourceConfigs.some(x => x)) this.importAggregator()
20
36
  else deregisterDrain(this.agentIdentifier, this.featureName)
@@ -184,9 +184,7 @@ export class Aggregate extends AggregateBase {
184
184
  if (!this.stackReported[bucketHash]) {
185
185
  this.stackReported[bucketHash] = true
186
186
  params.stack_trace = truncateSize(stackInfo.stackString)
187
- this.observedAt[bucketHash] = Math.floor(agentRuntime.timeKeeper.correctAbsoluteTimestamp(
188
- agentRuntime.timeKeeper.convertRelativeTimestamp(time)
189
- ))
187
+ this.observedAt[bucketHash] = Math.floor(agentRuntime.timeKeeper.correctRelativeTimestamp(time))
190
188
  } else {
191
189
  params.browser_stack_hash = stringHashCode(stackInfo.stackString)
192
190
  }
@@ -203,9 +201,7 @@ export class Aggregate extends AggregateBase {
203
201
  }
204
202
 
205
203
  params.firstOccurrenceTimestamp = this.observedAt[bucketHash]
206
- params.timestamp = Math.floor(agentRuntime.timeKeeper.correctAbsoluteTimestamp(
207
- agentRuntime.timeKeeper.convertRelativeTimestamp(time)
208
- ))
204
+ params.timestamp = Math.floor(agentRuntime.timeKeeper.correctRelativeTimestamp(time))
209
205
 
210
206
  var type = internal ? 'ierr' : 'err'
211
207
  var newMetrics = { time }
@@ -70,9 +70,7 @@ export class Aggregate extends AggregateBase {
70
70
  if (typeof message !== 'string' || !message) return warn(32)
71
71
 
72
72
  const log = new Log(
73
- Math.floor(this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(
74
- this.#agentRuntime.timeKeeper.convertRelativeTimestamp(timestamp)
75
- )),
73
+ Math.floor(this.#agentRuntime.timeKeeper.correctRelativeTimestamp(timestamp)),
76
74
  message,
77
75
  attributes,
78
76
  level
@@ -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
 
@@ -101,7 +102,7 @@ export class Aggregate extends AggregateBase {
101
102
  if (proxy.beacon) this.storeSupportabilityMetrics('Config/BeaconUrl/Changed')
102
103
 
103
104
  if (isBrowserScope && window.MutationObserver) {
104
- if (window.self !== window.top) { this.storeSupportabilityMetrics('Generic/Runtime/IFrame/Detected') }
105
+ if (isIFrameWindow(window)) { this.storeSupportabilityMetrics('Generic/Runtime/IFrame/Detected') }
105
106
  const preExistingVideos = window.document.querySelectorAll('video').length
106
107
  if (preExistingVideos) this.storeSupportabilityMetrics('Generic/VideoElement/Added', preExistingVideos)
107
108
  const preExistingIframes = window.document.querySelectorAll('iframe').length
@@ -106,9 +106,7 @@ export class Aggregate extends AggregateBase {
106
106
  queryParameters.fcp = firstContentfulPaint.current.value
107
107
 
108
108
  if (this.timeKeeper?.ready) {
109
- queryParameters.timestamp = Math.floor(this.timeKeeper.correctAbsoluteTimestamp(
110
- this.timeKeeper.convertRelativeTimestamp(now())
111
- ))
109
+ queryParameters.timestamp = Math.floor(this.timeKeeper.correctRelativeTimestamp(now()))
112
110
  }
113
111
 
114
112
  const rumStartTime = now()