@newrelic/browser-agent 1.239.1 → 1.241.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 (178) hide show
  1. package/README.md +4 -0
  2. package/dist/cjs/cdn/pro.js +3 -2
  3. package/dist/cjs/cdn/spa.js +4 -3
  4. package/dist/cjs/common/config/state/init.js +25 -17
  5. package/dist/cjs/common/constants/env.cdn.js +1 -1
  6. package/dist/cjs/common/constants/env.npm.js +1 -1
  7. package/dist/cjs/common/constants/runtime.js +9 -5
  8. package/dist/cjs/common/harvest/harvest.js +5 -3
  9. package/dist/cjs/common/vitals/constants.js +17 -0
  10. package/dist/cjs/common/vitals/cumulative-layout-shift.js +27 -0
  11. package/dist/cjs/common/vitals/first-contentful-paint.js +49 -0
  12. package/dist/cjs/common/vitals/first-input-delay.js +32 -0
  13. package/dist/{esm/features/page_view_timing → cjs/common/vitals}/first-paint.js +19 -17
  14. package/dist/cjs/common/vitals/interaction-to-next-paint.js +29 -0
  15. package/dist/cjs/common/vitals/largest-contentful-paint.js +41 -0
  16. package/dist/cjs/common/vitals/long-task.js +64 -0
  17. package/dist/cjs/common/vitals/time-to-first-byte.js +36 -0
  18. package/dist/cjs/common/vitals/vital-metric.js +71 -0
  19. package/dist/cjs/features/ajax/aggregate/index.js +4 -1
  20. package/dist/cjs/features/metrics/aggregate/index.js +17 -7
  21. package/dist/cjs/features/page_view_event/aggregate/index.js +18 -40
  22. package/dist/cjs/features/page_view_event/constants.js +2 -8
  23. package/dist/cjs/features/page_view_event/instrument/index.js +0 -22
  24. package/dist/cjs/features/page_view_timing/aggregate/index.js +36 -147
  25. package/dist/cjs/features/page_view_timing/instrument/index.js +0 -3
  26. package/dist/cjs/features/session_replay/aggregate/index.js +81 -35
  27. package/dist/cjs/features/session_trace/aggregate/index.js +13 -1
  28. package/dist/cjs/features/spa/aggregate/index.js +4 -3
  29. package/dist/cjs/loaders/agent.js +3 -0
  30. package/dist/cjs/loaders/api/api.js +2 -0
  31. package/dist/cjs/loaders/api/apiAsync.js +4 -2
  32. package/dist/cjs/loaders/browser-agent.js +2 -1
  33. package/dist/cjs/loaders/configure/configure.js +13 -1
  34. package/dist/cjs/loaders/configure/public-path.js +13 -0
  35. package/dist/cjs/loaders/configure/public-path.npm.js +10 -0
  36. package/dist/esm/cdn/pro.js +2 -1
  37. package/dist/esm/cdn/spa.js +2 -1
  38. package/dist/esm/common/config/state/init.js +25 -17
  39. package/dist/esm/common/constants/env.cdn.js +1 -1
  40. package/dist/esm/common/constants/env.npm.js +1 -1
  41. package/dist/esm/common/constants/runtime.js +5 -3
  42. package/dist/esm/common/harvest/harvest.js +6 -4
  43. package/dist/esm/common/vitals/constants.js +10 -0
  44. package/dist/esm/common/vitals/cumulative-layout-shift.js +20 -0
  45. package/dist/esm/common/vitals/first-contentful-paint.js +41 -0
  46. package/dist/esm/common/vitals/first-input-delay.js +25 -0
  47. package/dist/{cjs/features/page_view_timing → esm/common/vitals}/first-paint.js +12 -24
  48. package/dist/esm/common/vitals/interaction-to-next-paint.js +22 -0
  49. package/dist/esm/common/vitals/largest-contentful-paint.js +34 -0
  50. package/dist/esm/common/vitals/long-task.js +57 -0
  51. package/dist/esm/common/vitals/time-to-first-byte.js +29 -0
  52. package/dist/esm/common/vitals/vital-metric.js +64 -0
  53. package/dist/esm/features/ajax/aggregate/index.js +4 -1
  54. package/dist/esm/features/metrics/aggregate/index.js +18 -8
  55. package/dist/esm/features/page_view_event/aggregate/index.js +20 -42
  56. package/dist/esm/features/page_view_event/constants.js +1 -4
  57. package/dist/esm/features/page_view_event/instrument/index.js +0 -22
  58. package/dist/esm/features/page_view_timing/aggregate/index.js +37 -148
  59. package/dist/esm/features/page_view_timing/instrument/index.js +0 -3
  60. package/dist/esm/features/session_replay/aggregate/index.js +81 -35
  61. package/dist/esm/features/session_trace/aggregate/index.js +13 -1
  62. package/dist/esm/features/spa/aggregate/index.js +4 -3
  63. package/dist/esm/loaders/agent.js +2 -0
  64. package/dist/esm/loaders/api/api.js +2 -0
  65. package/dist/esm/loaders/api/apiAsync.js +5 -3
  66. package/dist/esm/loaders/browser-agent.js +2 -1
  67. package/dist/esm/loaders/configure/configure.js +13 -1
  68. package/dist/esm/loaders/configure/public-path.js +6 -0
  69. package/dist/esm/loaders/configure/public-path.npm.js +3 -0
  70. package/dist/types/common/config/state/init.d.ts.map +1 -1
  71. package/dist/types/common/constants/runtime.d.ts +3 -1
  72. package/dist/types/common/constants/runtime.d.ts.map +1 -1
  73. package/dist/types/common/harvest/harvest.d.ts +0 -1
  74. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  75. package/dist/types/common/vitals/constants.d.ts +11 -0
  76. package/dist/types/common/vitals/constants.d.ts.map +1 -0
  77. package/dist/types/common/vitals/cumulative-layout-shift.d.ts +3 -0
  78. package/dist/types/common/vitals/cumulative-layout-shift.d.ts.map +1 -0
  79. package/dist/types/common/vitals/first-contentful-paint.d.ts +3 -0
  80. package/dist/types/common/vitals/first-contentful-paint.d.ts.map +1 -0
  81. package/dist/types/common/vitals/first-input-delay.d.ts +3 -0
  82. package/dist/types/common/vitals/first-input-delay.d.ts.map +1 -0
  83. package/dist/types/common/vitals/first-paint.d.ts +3 -0
  84. package/dist/types/common/vitals/first-paint.d.ts.map +1 -0
  85. package/dist/types/common/vitals/interaction-to-next-paint.d.ts +3 -0
  86. package/dist/types/common/vitals/interaction-to-next-paint.d.ts.map +1 -0
  87. package/dist/types/common/vitals/largest-contentful-paint.d.ts +3 -0
  88. package/dist/types/common/vitals/largest-contentful-paint.d.ts.map +1 -0
  89. package/dist/types/common/vitals/long-task.d.ts +3 -0
  90. package/dist/types/common/vitals/long-task.d.ts.map +1 -0
  91. package/dist/types/common/vitals/time-to-first-byte.d.ts +3 -0
  92. package/dist/types/common/vitals/time-to-first-byte.d.ts.map +1 -0
  93. package/dist/types/common/vitals/vital-metric.d.ts +18 -0
  94. package/dist/types/common/vitals/vital-metric.d.ts.map +1 -0
  95. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  96. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  97. package/dist/types/features/page_view_event/aggregate/index.d.ts +3 -2
  98. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  99. package/dist/types/features/page_view_event/constants.d.ts +0 -3
  100. package/dist/types/features/page_view_event/constants.d.ts.map +1 -1
  101. package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
  102. package/dist/types/features/page_view_timing/aggregate/index.d.ts +1 -3
  103. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  104. package/dist/types/features/page_view_timing/instrument/index.d.ts.map +1 -1
  105. package/dist/types/features/session_replay/aggregate/index.d.ts +16 -4
  106. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  107. package/dist/types/features/session_trace/aggregate/index.d.ts +9 -1
  108. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  109. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  110. package/dist/types/loaders/agent.d.ts.map +1 -1
  111. package/dist/types/loaders/api/api.d.ts.map +1 -1
  112. package/dist/types/loaders/api/apiAsync.d.ts.map +1 -1
  113. package/dist/types/loaders/browser-agent.d.ts.map +1 -1
  114. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  115. package/dist/types/loaders/configure/public-path.d.ts +2 -0
  116. package/dist/types/loaders/configure/public-path.d.ts.map +1 -0
  117. package/dist/types/loaders/configure/public-path.npm.d.ts +2 -0
  118. package/dist/types/loaders/configure/public-path.npm.d.ts.map +1 -0
  119. package/package.json +2 -3
  120. package/src/cdn/pro.js +2 -0
  121. package/src/cdn/spa.js +2 -0
  122. package/src/common/config/state/init.js +21 -17
  123. package/src/common/constants/runtime.js +7 -3
  124. package/src/common/constants/runtime.test.js +8 -0
  125. package/src/common/harvest/harvest-scheduler.test.js +2 -2
  126. package/src/common/harvest/harvest.js +6 -4
  127. package/src/common/harvest/harvest.test.js +17 -0
  128. package/src/common/vitals/__mocks__/web-vitals.js +19 -0
  129. package/src/common/vitals/constants.js +10 -0
  130. package/src/common/vitals/cumulative-layout-shift.js +13 -0
  131. package/src/common/vitals/cumulative-layout-shift.test.js +71 -0
  132. package/src/common/vitals/first-contentful-paint.js +31 -0
  133. package/src/common/vitals/first-contentful-paint.test.js +124 -0
  134. package/src/common/vitals/first-input-delay.js +20 -0
  135. package/src/common/vitals/first-input-delay.test.js +88 -0
  136. package/src/{features/page_view_timing → common/vitals}/first-paint.js +11 -17
  137. package/src/common/vitals/first-paint.test.js +127 -0
  138. package/src/common/vitals/interaction-to-next-paint.js +13 -0
  139. package/src/common/vitals/interaction-to-next-paint.test.js +74 -0
  140. package/src/common/vitals/largest-contentful-paint.js +29 -0
  141. package/src/common/vitals/largest-contentful-paint.test.js +94 -0
  142. package/src/common/vitals/long-task.js +52 -0
  143. package/src/common/vitals/long-task.test.js +122 -0
  144. package/src/common/vitals/time-to-first-byte.js +21 -0
  145. package/src/common/vitals/time-to-first-byte.test.js +147 -0
  146. package/src/common/vitals/vital-metric.js +60 -0
  147. package/src/common/vitals/vital-metric.test.js +171 -0
  148. package/src/features/ajax/aggregate/index.js +5 -1
  149. package/src/features/metrics/aggregate/index.js +11 -4
  150. package/src/features/page_view_event/aggregate/index.js +20 -43
  151. package/src/features/page_view_event/constants.js +0 -3
  152. package/src/features/page_view_event/instrument/index.js +0 -21
  153. package/src/features/page_view_timing/aggregate/index.component-test.js +86 -0
  154. package/src/features/page_view_timing/aggregate/index.js +31 -108
  155. package/src/features/page_view_timing/instrument/index.js +0 -3
  156. package/src/features/session_replay/aggregate/index.component-test.js +10 -10
  157. package/src/features/session_replay/aggregate/index.js +62 -29
  158. package/src/features/session_trace/aggregate/index.js +15 -1
  159. package/src/features/spa/aggregate/index.js +4 -3
  160. package/src/loaders/agent.js +2 -0
  161. package/src/loaders/api/api.js +2 -0
  162. package/src/loaders/api/apiAsync.js +5 -4
  163. package/src/loaders/browser-agent.js +3 -1
  164. package/src/loaders/configure/configure.js +15 -7
  165. package/src/loaders/configure/public-path.js +6 -0
  166. package/src/loaders/configure/public-path.npm.js +4 -0
  167. package/dist/cjs/common/metrics/paint-metrics.js +0 -13
  168. package/dist/cjs/features/page_view_timing/long-tasks.js +0 -75
  169. package/dist/esm/common/metrics/paint-metrics.js +0 -6
  170. package/dist/esm/features/page_view_timing/long-tasks.js +0 -69
  171. package/dist/types/common/metrics/paint-metrics.d.ts +0 -2
  172. package/dist/types/common/metrics/paint-metrics.d.ts.map +0 -1
  173. package/dist/types/features/page_view_timing/first-paint.d.ts +0 -2
  174. package/dist/types/features/page_view_timing/first-paint.d.ts.map +0 -1
  175. package/dist/types/features/page_view_timing/long-tasks.d.ts +0 -2
  176. package/dist/types/features/page_view_timing/long-tasks.d.ts.map +0 -1
  177. package/src/common/metrics/paint-metrics.js +0 -6
  178. package/src/features/page_view_timing/long-tasks.js +0 -60
@@ -67,6 +67,9 @@ export class Aggregate extends AggregateBase {
67
67
 
68
68
  this.drain()
69
69
 
70
+ const beacon = getInfo(agentIdentifier).errorBeacon
71
+ const proxyBeacon = agentInit.proxy.beacon
72
+
70
73
  function storeXhr (params, metrics, startTime, endTime, type) {
71
74
  metrics.time = startTime
72
75
 
@@ -86,7 +89,8 @@ export class Aggregate extends AggregateBase {
86
89
  if (!allAjaxIsEnabled) return
87
90
 
88
91
  if (!shouldCollectEvent(params)) {
89
- if (params.hostname === getInfo(agentIdentifier).errorBeacon) {
92
+ if (params.hostname === beacon || (proxyBeacon && params.hostname === proxyBeacon)) {
93
+ // This doesn't make a distinction if the same-domain request is going to a different port or path...
90
94
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Excluded/Agent'], undefined, FEATURE_NAMES.metrics, ee)
91
95
  } else {
92
96
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Excluded/App'], undefined, FEATURE_NAMES.metrics, ee)
@@ -1,4 +1,4 @@
1
- import { getRuntime } from '../../../common/config/config'
1
+ import { getRuntime, getConfiguration } from '../../../common/config/config'
2
2
  import { registerHandler } from '../../../common/event-emitter/register-handler'
3
3
  import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
4
4
  import { FEATURE_NAME, SUPPORTABILITY_METRIC, CUSTOM_METRIC, SUPPORTABILITY_METRIC_CHANNEL, CUSTOM_METRIC_CHANNEL } from '../constants'
@@ -29,9 +29,11 @@ export class Aggregate extends AggregateBase {
29
29
  this.singleChecks() // checks that are run only one time, at script load
30
30
  this.eachSessionChecks() // the start of every time user engages with page
31
31
 
32
- // *cli, Mar 23 - Per NR-94597, this feature should only harvest ONCE at the (potential) EoL time of the page.
33
- scheduler = new HarvestScheduler('jserrors', { onUnload: () => this.unload() }, this)
34
- scheduler.harvest.on('jserrors', () => ({ body: this.aggregator.take(['cm', 'sm']) }))
32
+ this.ee.on(`drain-${this.featureName}`, () => {
33
+ // *cli, Mar 23 - Per NR-94597, this feature should only harvest ONCE at the (potential) EoL time of the page.
34
+ scheduler = new HarvestScheduler('jserrors', { onUnload: () => this.unload() }, this)
35
+ scheduler.harvest.on('jserrors', () => ({ body: this.aggregator.take(['cm', 'sm']) }))
36
+ }) // this is needed to ensure EoL is "on" and sent
35
37
 
36
38
  this.drain()
37
39
  }
@@ -82,6 +84,11 @@ export class Aggregate extends AggregateBase {
82
84
  const rules = getRules(this.agentIdentifier)
83
85
  if (rules.length > 0) this.storeSupportabilityMetrics('Generic/Obfuscate/Detected')
84
86
  if (rules.length > 0 && !validateRules(rules)) this.storeSupportabilityMetrics('Generic/Obfuscate/Invalid')
87
+
88
+ // Check if proxy for either chunks or beacon is being used
89
+ const { proxy } = getConfiguration(this.agentIdentifier)
90
+ if (proxy.assets) this.storeSupportabilityMetrics('Config/AssetsUrl/Changed')
91
+ if (proxy.beacon) this.storeSupportabilityMetrics('Config/BeaconUrl/Changed')
85
92
  }
86
93
 
87
94
  eachSessionChecks () {
@@ -1,53 +1,41 @@
1
- import { handle } from '../../../common/event-emitter/handle'
2
- import { FEATURE_NAMES } from '../../../loaders/features/features'
3
- import { isiOS, globalScope, isBrowserScope } from '../../../common/constants/runtime'
4
- import { onTTFB } from 'web-vitals'
1
+ import { globalScope, isBrowserScope } from '../../../common/constants/runtime'
5
2
  import { addPT, addPN } from '../../../common/timing/nav-timing'
6
3
  import { stringify } from '../../../common/util/stringify'
7
- import { paintMetrics } from '../../../common/metrics/paint-metrics'
8
- import { getConfigurationValue, getInfo, getRuntime } from '../../../common/config/config'
4
+ import { getInfo, getRuntime } from '../../../common/config/config'
9
5
  import { Harvest } from '../../../common/harvest/harvest'
10
6
  import * as CONSTANTS from '../constants'
11
7
  import { getActivatedFeaturesFlags } from './initialized-features'
12
8
  import { activateFeatures } from '../../../common/util/feature-flags'
13
9
  import { warn } from '../../../common/util/console'
14
10
  import { AggregateBase } from '../../utils/aggregate-base'
11
+ import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
12
+ import { firstPaint } from '../../../common/vitals/first-paint'
13
+ import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
15
14
 
16
15
  export class Aggregate extends AggregateBase {
17
16
  static featureName = CONSTANTS.FEATURE_NAME
18
17
  constructor (agentIdentifier, aggregator) {
19
18
  super(agentIdentifier, aggregator, CONSTANTS.FEATURE_NAME)
20
19
 
21
- if (typeof PerformanceNavigationTiming !== 'undefined' && !isiOS) {
22
- this.alreadySent = false // we don't support timings on BFCache restores
23
- const agentRuntime = getRuntime(agentIdentifier) // we'll store timing values on the runtime obj to be read by the aggregate module
20
+ this.timeToFirstByte = 0
21
+ this.firstByteToWindowLoad = 0 // our "frontend" duration
22
+ this.firstByteToDomContent = 0 // our "dom processing" duration
24
23
 
25
- /* Time To First Byte
26
- This listener must record these values *before* PVE's aggregate sends RUM. */
27
- onTTFB(({ value, entries }) => {
28
- if (this.alreadySent) return
29
- this.alreadySent = true
30
-
31
- agentRuntime[CONSTANTS.TTFB] = Math.round(value) // this is our "backend" duration; web-vitals will ensure it's lower bounded at 0
32
-
33
- // Similar to what vitals does for ttfb, we have to factor in activation-start when calculating relative timings:
24
+ if (isBrowserScope) {
25
+ timeToFirstByte.subscribe(({ value, entries }) => {
34
26
  const navEntry = entries[0]
35
- const respOrActivStart = Math.max(navEntry.responseStart, navEntry.activationStart || 0)
36
- agentRuntime[CONSTANTS.FBTWL] = Math.max(Math.round(navEntry.loadEventEnd - respOrActivStart), 0) // our "frontend" duration
37
- handle('timing', ['load', Math.round(navEntry.loadEventEnd)], undefined, FEATURE_NAMES.pageViewTiming, this.ee)
38
- agentRuntime[CONSTANTS.FBTDC] = Math.max(Math.round(navEntry.domContentLoadedEventEnd - respOrActivStart), 0) // our "dom processing" duration
27
+ this.timeToFirstByte = Math.max(value, this.timeToFirstByte)
28
+ this.firstByteToWindowLoad = Math.max(Math.round(navEntry.loadEventEnd - this.timeToFirstByte), this.firstByteToWindowLoad) // our "frontend" duration
29
+ this.firstByteToDomContent = Math.max(Math.round(navEntry.domContentLoadedEventEnd - this.timeToFirstByte), this.firstByteToDomContent) // our "dom processing" duration
39
30
 
40
31
  this.sendRum()
41
32
  })
42
33
  } else {
43
- this.sendRum() // timings either already in runtime from instrument or is meant to get 0'd.
34
+ // worker agent build does not get TTFB values, use default 0 values
35
+ this.sendRum()
44
36
  }
45
37
  }
46
38
 
47
- getScheme () {
48
- return getConfigurationValue(this.agentIdentifier, 'ssl') === false ? 'http' : 'https'
49
- }
50
-
51
39
  sendRum () {
52
40
  const info = getInfo(this.agentIdentifier)
53
41
  const agentRuntime = getRuntime(this.agentIdentifier)
@@ -60,9 +48,9 @@ export class Aggregate extends AggregateBase {
60
48
  // These 3 values should've been recorded after load and before this func runs. They are part of the minimum required for PageView events to be created.
61
49
  // Following PR #428, which demands that all agents send RUM call, these need to be sent even outside of the main window context where PerformanceTiming
62
50
  // or PerformanceNavigationTiming do not exists. Hence, they'll be filled in by 0s instead in, for example, worker threads that still init the PVE module.
63
- this.aggregator.store('measures', 'be', { value: isBrowserScope ? agentRuntime[CONSTANTS.TTFB] : 0 })
64
- this.aggregator.store('measures', 'fe', { value: isBrowserScope ? agentRuntime[CONSTANTS.FBTWL] : 0 })
65
- this.aggregator.store('measures', 'dc', { value: isBrowserScope ? agentRuntime[CONSTANTS.FBTDC] : 0 })
51
+ this.aggregator.store('measures', 'be', { value: this.timeToFirstByte })
52
+ this.aggregator.store('measures', 'fe', { value: this.firstByteToWindowLoad })
53
+ this.aggregator.store('measures', 'dc', { value: this.firstByteToDomContent })
66
54
 
67
55
  const queryParameters = {
68
56
  tt: info.ttGuid,
@@ -103,19 +91,8 @@ export class Aggregate extends AggregateBase {
103
91
  }
104
92
  }
105
93
 
106
- try { // PVTiming sends these too, albeit using web-vitals and slightly different; it's unknown why they're duplicated, but PVT should be the truth
107
- var entries = globalScope.performance.getEntriesByType('paint')
108
- entries.forEach(function (entry) {
109
- if (!entry.startTime || entry.startTime <= 0) return
110
-
111
- if (entry.name === 'first-paint') {
112
- queryParameters.fp = String(Math.floor(entry.startTime))
113
- } else if (entry.name === 'first-contentful-paint') {
114
- queryParameters.fcp = String(Math.floor(entry.startTime))
115
- }
116
- paintMetrics[entry.name] = Math.floor(entry.startTime) // this is consumed by Spa module
117
- })
118
- } catch (e) {}
94
+ queryParameters.fp = firstPaint.current.value
95
+ queryParameters.fcp = firstContentfulPaint.current.value
119
96
 
120
97
  harvester.send({
121
98
  endpoint: 'rum',
@@ -1,6 +1,3 @@
1
1
  import { FEATURE_NAMES } from '../../loaders/features/features'
2
2
 
3
3
  export const FEATURE_NAME = FEATURE_NAMES.pageViewEvent
4
- export const TTFB = 'firstbyte'
5
- export const FBTDC = 'domcontent'
6
- export const FBTWL = 'windowload'
@@ -1,32 +1,11 @@
1
- import { handle } from '../../../common/event-emitter/handle'
2
- import { isiOS } from '../../../common/constants/runtime'
3
1
  import { InstrumentBase } from '../../utils/instrument-base'
4
2
  import * as CONSTANTS from '../constants'
5
- import { FEATURE_NAMES } from '../../../loaders/features/features'
6
- import { getRuntime } from '../../../common/config/config'
7
- import { onDOMContentLoaded, onWindowLoad } from '../../../common/window/load'
8
- import { now } from '../../../common/timing/now'
9
3
 
10
4
  export class Instrument extends InstrumentBase {
11
5
  static featureName = CONSTANTS.FEATURE_NAME
12
6
  constructor (agentIdentifier, aggregator, auto = true) {
13
7
  super(agentIdentifier, aggregator, CONSTANTS.FEATURE_NAME, auto)
14
8
 
15
- if ((typeof PerformanceNavigationTiming === 'undefined' || isiOS) && typeof PerformanceTiming !== 'undefined') {
16
- // For majority browser versions in which PNT exists, we can get load timings later from the nav entry (in the aggregate portion). At minimum, PT should exist for main window.
17
- // *cli Mar'23 - iOS 15.2 & 15.4 testing in Sauce still fails with onTTFB. Hence, all iOS will fallback to this for now. Unknown if this is similar in nature to iOSBelow16 bug.
18
- const agentRuntime = getRuntime(agentIdentifier)
19
-
20
- agentRuntime[CONSTANTS.TTFB] = Math.max(Date.now() - agentRuntime.offset, 0)
21
- onDOMContentLoaded(() => { agentRuntime[CONSTANTS.FBTDC] = Math.max(now() - agentRuntime[CONSTANTS.TTFB], 0) })
22
- onWindowLoad(() => {
23
- const timeNow = now()
24
- agentRuntime[CONSTANTS.FBTWL] = Math.max(timeNow - agentRuntime[CONSTANTS.TTFB], 0)
25
- handle('timing', ['load', timeNow], undefined, FEATURE_NAMES.pageViewTiming, this.ee)
26
- })
27
- }
28
- // Else, inference: inside worker or some other env where these events are irrelevant. They'll get filled in with 0s in RUM call.
29
-
30
9
  this.importAggregator()
31
10
  }
32
11
  }
@@ -0,0 +1,86 @@
1
+ import { Aggregator } from '../../../common/aggregate/aggregator'
2
+ import { ee } from '../../../common/event-emitter/contextual-ee'
3
+ import { drain } from '../../../common/drain/drain'
4
+ import { setRuntime } from '../../../common/config/config'
5
+
6
+ jest.mock('web-vitals', () => ({
7
+ __esModule: true,
8
+ // eslint-disable-next-line
9
+ onFID: jest.fn(cb => cb({
10
+ value: 1234,
11
+ entries: [{ name: 'pointerdown', startTime: 5 }]
12
+ })),
13
+ // eslint-disable-next-line
14
+ onCLS: jest.fn((cb) => cb({
15
+ value: 1,
16
+ entries: [{ value: 1 }]
17
+ })),
18
+ // eslint-disable-next-line
19
+ onFCP: jest.fn((cb) => cb({
20
+ value: 1,
21
+ entries: [{ value: 1 }]
22
+ })),
23
+ // eslint-disable-next-line
24
+ onINP: jest.fn((cb) => cb({
25
+ value: 1,
26
+ entries: [{ value: 1 }]
27
+ })),
28
+ // eslint-disable-next-line
29
+ onLCP: jest.fn((cb) => cb({
30
+ value: 1,
31
+ entries: [{ value: 1 }]
32
+ }))
33
+ })
34
+ )
35
+
36
+ let pvtAgg, cumulativeLayoutShift
37
+ describe('pvt aggregate tests', () => {
38
+ beforeEach(async () => {
39
+ const { Aggregate } = await import('.')
40
+ setRuntime('abcd', {})
41
+ pvtAgg = new Aggregate('abcd', new Aggregator({ agentIdentifier: 'abcd', ee }))
42
+ pvtAgg.scheduler.harvest.send = jest.fn()
43
+ pvtAgg.prepareHarvest = jest.fn(() => ({}))
44
+ drain('abcd', 'feature')
45
+
46
+ global.navigator.connection = {
47
+ type: 'cellular',
48
+ effectiveType: '3g',
49
+ rtt: 270,
50
+ downlink: 700
51
+ }
52
+
53
+ const { cumulativeLayoutShift: cls } = await import('../../../common/vitals/cumulative-layout-shift')
54
+ cumulativeLayoutShift = cls
55
+ })
56
+ test('LCP event with CLS attribute', () => {
57
+ cumulativeLayoutShift.update({ value: 1 })
58
+ pvtAgg.addTiming('lcp', 1, { size: 1, startTime: 1 })
59
+
60
+ var timing = find(pvtAgg.timings, function (t) {
61
+ return t.name === 'lcp'
62
+ })
63
+
64
+ expect(timing.attrs.cls).toEqual(1) // 'CLS value should be the one present at the time LCP happened'
65
+
66
+ function find (arr, fn) {
67
+ if (arr.find) {
68
+ return arr.find(fn)
69
+ }
70
+ var match = null
71
+ arr.forEach(function (t) {
72
+ if (fn(t)) {
73
+ match = t
74
+ }
75
+ })
76
+ return match
77
+ }
78
+ })
79
+
80
+ test('sends expected FI attributes when available', () => {
81
+ expect(pvtAgg.timings.length).toBeGreaterThanOrEqual(1)
82
+ const fiPayload = pvtAgg.timings.find(x => x.name === 'fi')
83
+ expect(fiPayload.value).toEqual(5)
84
+ expect(fiPayload.attrs).toEqual(expect.objectContaining({ type: 'pointerdown', fid: 1234, cls: 1 }))
85
+ })
86
+ })
@@ -3,146 +3,69 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
 
6
- import { onFCP, onFID, onLCP, onCLS, onINP } from 'web-vitals'
7
- import { onFirstPaint } from '../first-paint'
8
- import { onLongTask } from '../long-tasks'
9
- import { iOSBelow16 } from '../../../common/constants/runtime'
10
6
  import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
11
7
  import { mapOwn } from '../../../common/util/map-own'
12
8
  import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
13
9
  import { registerHandler } from '../../../common/event-emitter/register-handler'
14
- import { cleanURL } from '../../../common/url/clean-url'
15
10
  import { handle } from '../../../common/event-emitter/handle'
16
- import { getInfo, getConfigurationValue, getRuntime } from '../../../common/config/config'
11
+ import { getInfo, getConfigurationValue } from '../../../common/config/config'
17
12
  import { FEATURE_NAME } from '../constants'
18
13
  import { FEATURE_NAMES } from '../../../loaders/features/features'
19
14
  import { AggregateBase } from '../../utils/aggregate-base'
15
+ import { cumulativeLayoutShift } from '../../../common/vitals/cumulative-layout-shift'
16
+ import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
17
+ import { firstInputDelay } from '../../../common/vitals/first-input-delay'
18
+ import { firstPaint } from '../../../common/vitals/first-paint'
19
+ import { interactionToNextPaint } from '../../../common/vitals/interaction-to-next-paint'
20
+ import { largestContentfulPaint } from '../../../common/vitals/largest-contentful-paint'
21
+ import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
22
+ import { longTask } from '../../../common/vitals/long-task'
20
23
 
21
24
  export class Aggregate extends AggregateBase {
22
25
  static featureName = FEATURE_NAME
26
+
27
+ #handleVitalMetric = ({ name, value, attrs }) => {
28
+ this.addTiming(name, value, attrs)
29
+ }
30
+
23
31
  constructor (agentIdentifier, aggregator) {
24
32
  super(agentIdentifier, aggregator, FEATURE_NAME)
25
33
 
26
34
  this.timings = []
27
35
  this.timingsSent = []
28
36
  this.curSessEndRecorded = false
29
- this.cls = null // this should be null unless set to a numeric value by web-vitals so that we differentiate if CLS is supported
30
-
31
- /* ! This is the section that used to be in the loader portion: ! */
32
- /* ------------------------------------------------------------ */
33
- const pageStartedHidden = getRuntime(agentIdentifier).initHidden // our attempt at recapturing initial vis state since this code runs post-load time
34
- this.alreadySent = new Set() // since we don't support timings on BFCache restores, this tracks and helps cap metrics that web-vitals report more than once
35
-
36
- /* PerformancePaintTiming API - BFC is not yet supported. */
37
- onFirstPaint(({ name, value }) => {
38
- if (pageStartedHidden) return
39
- this.addTiming(name.toLowerCase(), Math.floor(value))
40
- })
41
-
42
- /* First Contentful Paint - As of WV v3, it still imperfectly tries to detect document vis state asap and isn't supposed to report if page starts hidden. */
43
- if (iOSBelow16) {
44
- try {
45
- if (!pageStartedHidden) { // see ios-version.js for detail on this following bug case; tldr: buffered flag doesn't work but getEntriesByType does
46
- const paintEntries = performance.getEntriesByType('paint')
47
- paintEntries.forEach(entry => {
48
- if (entry.name === 'first-contentful-paint') {
49
- this.addTiming('fcp', Math.floor(entry.startTime))
50
- }
51
- })
52
- }
53
- } catch (e) {}
54
- } else {
55
- onFCP(({ name, value }) => {
56
- if (pageStartedHidden || this.alreadySent.has(name)) return
57
- this.alreadySent.add(name)
58
- this.addTiming(name.toLowerCase(), value)
59
- })
60
- }
61
-
62
- /* First Input Delay (+"First Interaction") - As of WV v3, it still imperfectly tries to detect document vis state asap and isn't supposed to report if page starts hidden. */
63
- onFID(({ name, value, entries }) => {
64
- if (pageStartedHidden || this.alreadySent.has(name) || entries.length === 0) return
65
- this.alreadySent.add(name)
66
-
67
- // CWV will only report one (THE) first-input entry to us; fid isn't reported if there are no user interactions occurs before the *first* page hiding.
68
- const fiEntry = entries[0]
69
- const attributes = {
70
- type: fiEntry.name,
71
- fid: Math.round(value)
72
- }
73
- this.addConnectionAttributes(attributes)
74
- this.addTiming('fi', Math.round(fiEntry.startTime), attributes)
75
- })
76
-
77
- /* Largest Contentful Paint - As of WV v3, it still imperfectly tries to detect document vis state asap and isn't supposed to report if page starts hidden. */
78
- onLCP(({ name, value, entries }) => {
79
- if (pageStartedHidden || this.alreadySent.has(name)) return
80
- this.alreadySent.add(name)
81
-
82
- const attributes = {}
83
- if (entries.length > 0) {
84
- // CWV will only ever report one (THE) lcp entry to us; lcp is also only reported *once* on earlier(user interaction, page hidden).
85
- const lcpEntry = entries[entries.length - 1] // this looks weird if we only expect one, but this is how cwv-attribution gets it so to be sure...
86
- attributes.size = lcpEntry.size
87
- attributes.eid = lcpEntry.id
88
-
89
- if (lcpEntry.url) {
90
- attributes.elUrl = cleanURL(lcpEntry.url)
91
- }
92
- if (lcpEntry.element?.tagName) {
93
- attributes.elTag = lcpEntry.element.tagName
94
- }
95
- }
96
37
 
97
- this.addConnectionAttributes(attributes)
98
- this.addTiming(name.toLowerCase(), value, attributes)
38
+ firstPaint.subscribe(this.#handleVitalMetric)
39
+ firstContentfulPaint.subscribe(this.#handleVitalMetric)
40
+ firstInputDelay.subscribe(this.#handleVitalMetric)
41
+ largestContentfulPaint.subscribe(this.#handleVitalMetric)
42
+ interactionToNextPaint.subscribe(this.#handleVitalMetric)
43
+ timeToFirstByte.subscribe(({ entries }) => {
44
+ this.addTiming('load', Math.round(entries[0].loadEventEnd))
99
45
  })
100
46
 
101
- /* Cumulative Layout Shift - We don't have to limit this callback since cls is stored as a state and only sent as attribute on other timings.
102
- reportAllChanges ensures our tracked cls has the most recent rolling value to attach to 'unload' and 'pagehide'. */
103
- onCLS(({ value }) => { this.cls = value }, { reportAllChanges: true })
104
-
105
- /* Interaction-to-Next-Paint */
106
- onINP(({ name, value, id }) => this.addTiming(name.toLowerCase(), value, { metricId: id }))
107
-
108
- /* PerformanceLongTaskTiming API */
109
- if (getConfigurationValue(this.agentIdentifier, 'page_view_timing.long_task') === true) {
110
- onLongTask(({ name, value, info }) => this.addTiming(name.toLowerCase(), value, info)) // lt context is passed as 'info'=attrs in the timing node
111
- }
112
- /* ------------------------------------End of ex-loader section */
47
+ if (getConfigurationValue(this.agentIdentifier, 'page_view_timing.long_task') === true) longTask.subscribe(this.#handleVitalMetric)
113
48
 
114
49
  /* It's important that CWV api, like "onLCP", is called before this scheduler is initialized. The reason is because they listen to the same
115
50
  on vis change or pagehide events, and we'd want ex. onLCP to record the timing (win the race) before we try to send "final harvest". */
116
- this.scheduler = new HarvestScheduler('events', {
117
- onFinished: (...args) => this.onHarvestFinished(...args),
118
- getPayload: (...args) => this.prepareHarvest(...args)
119
- }, this)
120
51
 
121
- registerHandler('timing', (name, value, attrs) => this.addTiming(name, value, attrs), this.featureName, this.ee) // notice CLS is added to all timings via 4th param
122
52
  registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee)
123
53
  registerHandler('winPagehide', msTimestamp => this.recordPageUnload(msTimestamp), this.featureName, this.ee)
124
54
 
125
55
  const initialHarvestSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.initialHarvestSeconds') || 10
126
56
  const harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.harvestTimeSeconds') || 30
127
57
  // send initial data sooner, then start regular
128
- this.ee.on(`drain-${this.featureName}`, () => { this.scheduler.startTimer(harvestTimeSeconds, initialHarvestSeconds) })
58
+ this.ee.on(`drain-${this.featureName}`, () => {
59
+ this.scheduler = new HarvestScheduler('events', {
60
+ onFinished: (...args) => this.onHarvestFinished(...args),
61
+ getPayload: (...args) => this.prepareHarvest(...args)
62
+ }, this)
63
+ this.scheduler.startTimer(harvestTimeSeconds, initialHarvestSeconds)
64
+ })
129
65
 
130
66
  this.drain()
131
67
  }
132
68
 
133
- // takes an attributes object and appends connection attributes if available
134
- addConnectionAttributes (attributes) {
135
- var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection // to date, both window & worker shares the same support for connection
136
- if (!connection) return
137
-
138
- if (connection.type) attributes['net-type'] = connection.type
139
- if (connection.effectiveType) attributes['net-etype'] = connection.effectiveType
140
- if (connection.rtt) attributes['net-rtt'] = connection.rtt
141
- if (connection.downlink) attributes['net-dlink'] = connection.downlink
142
-
143
- return attributes
144
- }
145
-
146
69
  /**
147
70
  * Add the time of _document visibilitychange to hidden_ to the next PVT harvest == NRDB pageHide attr.
148
71
  * @param {number} timestamp
@@ -179,8 +102,8 @@ export class Aggregate extends AggregateBase {
179
102
  Mitigation: We've set initial CLS to null so that it's omitted from timings like 'pageHide' in that edge case. It should only be included if onCLS callback was executed at least once.
180
103
  Future: onCLS value changes should be reported directly & CLS separated into its own timing node so it's not beholden to 'pageHide' firing. It'd also be possible to report the real final CLS.
181
104
  */
182
- if (this.cls !== null) {
183
- attrs.cls = this.cls
105
+ if (cumulativeLayoutShift.current.value >= 0) {
106
+ attrs.cls = cumulativeLayoutShift.current.value
184
107
  }
185
108
 
186
109
  this.timings.push({
@@ -3,7 +3,6 @@
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
5
  import { handle } from '../../../common/event-emitter/handle'
6
- import { getRuntime } from '../../../common/config/config'
7
6
  import { subscribeToVisibilityChange } from '../../../common/window/page-visibility'
8
7
  import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
9
8
  import { now } from '../../../common/timing/now'
@@ -17,9 +16,7 @@ export class Instrument extends InstrumentBase {
17
16
  super(agentIdentifier, aggregator, FEATURE_NAME, auto)
18
17
  if (!isBrowserScope) return // CWV is irrelevant outside web context
19
18
 
20
- // Document visibility state becomes hidden; this should run as soon as possible in page life.
21
19
  // While we try to replicate web-vital's visibilitywatcher logic in an effort to defer that library to post-pageload, this isn't perfect and doesn't consider prerendering.
22
- getRuntime(agentIdentifier).initHidden = Boolean(document.visibilityState === 'hidden')
23
20
  subscribeToVisibilityChange(() => handle('docHidden', [now()], undefined, FEATURE_NAME, this.ee), true)
24
21
 
25
22
  // Window fires its pagehide event (typically on navigation--this occurrence is a *subset* of vis change); don't defer this unless it's guarantee it cannot happen before load(?)
@@ -39,7 +39,7 @@ class LocalMemory {
39
39
  let sr, session
40
40
  const agentIdentifier = 'abcd'
41
41
  const info = { licenseKey: 1234, applicationID: 9876 }
42
- const init = { session_replay: { enabled: true, sampleRate: 1, errorSampleRate: 0 } }
42
+ const init = { session_replay: { enabled: true, sampling_rate: 100, error_sampling_rate: 0 } }
43
43
 
44
44
  const anyQuery = {
45
45
  browser_monitoring_key: info.licenseKey,
@@ -99,14 +99,14 @@ describe('Session Replay', () => {
99
99
  })
100
100
 
101
101
  test('Session SR mode matches SR mode -- ERROR', async () => {
102
- setConfiguration(agentIdentifier, { session_replay: { sampleRate: 0, errorSampleRate: 1 } })
102
+ setConfiguration(agentIdentifier, { session_replay: { sampling_rate: 0, error_sampling_rate: 100 } })
103
103
  sr.ee.emit('rumresp-sr', [true])
104
104
  await wait(1)
105
105
  expect(session.state.sessionReplay).toEqual(sr.mode)
106
106
  })
107
107
 
108
108
  test('Session SR mode matches SR mode -- OFF', async () => {
109
- setConfiguration(agentIdentifier, { session_replay: { sampleRate: 0, errorSampleRate: 0 } })
109
+ setConfiguration(agentIdentifier, { session_replay: { sampling_rate: 0, error_sampling_rate: 0 } })
110
110
  sr.ee.emit('rumresp-sr', [true])
111
111
  await wait(1)
112
112
  expect(session.state.sessionReplay).toEqual(sr.mode)
@@ -149,28 +149,28 @@ describe('Session Replay', () => {
149
149
 
150
150
  describe('Session Replay Sample -> Mode Behaviors', () => {
151
151
  test('New Session -- Full 1 Error 1 === FULL', async () => {
152
- setConfiguration(agentIdentifier, { session_replay: { errorSampleRate: 1, sampleRate: 1 } })
152
+ setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 100 } })
153
153
  sr.ee.emit('rumresp-sr', [true])
154
154
  await wait(1)
155
155
  expect(sr.mode).toEqual(MODE.FULL)
156
156
  })
157
157
 
158
158
  test('New Session -- Full 1 Error 0 === FULL', async () => {
159
- setConfiguration(agentIdentifier, { session_replay: { errorSampleRate: 0, sampleRate: 1 } })
159
+ setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 0, sampling_rate: 100 } })
160
160
  sr.ee.emit('rumresp-sr', [true])
161
161
  await wait(1)
162
162
  expect(sr.mode).toEqual(MODE.FULL)
163
163
  })
164
164
 
165
165
  test('New Session -- Full 0 Error 1 === ERROR', async () => {
166
- setConfiguration(agentIdentifier, { session_replay: { errorSampleRate: 1, sampleRate: 0 } })
166
+ setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 0 } })
167
167
  sr.ee.emit('rumresp-sr', [true])
168
168
  await wait(1)
169
169
  expect(sr.mode).toEqual(MODE.ERROR)
170
170
  })
171
171
 
172
172
  test('New Session -- Full 0 Error 0 === OFF', async () => {
173
- setConfiguration(agentIdentifier, { session_replay: { errorSampleRate: 0, sampleRate: 0 } })
173
+ setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 0, sampling_rate: 0 } })
174
174
  sr.ee.emit('rumresp-sr', [true])
175
175
  await wait(1)
176
176
  expect(sr.mode).toEqual(MODE.OFF)
@@ -182,7 +182,7 @@ describe('Session Replay', () => {
182
182
  expect(session.isNew).toBeFalsy()
183
183
  primeSessionAndReplay(session)
184
184
  // configure to get "error" sample ---> but should inherit FULL from session manager
185
- setConfiguration(agentIdentifier, { session_replay: { errorSampleRate: 1, sampleRate: 0 } })
185
+ setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 0 } })
186
186
  sr.ee.emit('rumresp-sr', [true])
187
187
  await wait(1)
188
188
  expect(sr.mode).toEqual(MODE.FULL)
@@ -191,7 +191,7 @@ describe('Session Replay', () => {
191
191
 
192
192
  describe('Session Replay Error Mode Behaviors', () => {
193
193
  test('An error BEFORE rrweb import starts running in FULL from beginning', async () => {
194
- setConfiguration(agentIdentifier, { session_replay: { errorSampleRate: 1, sampleRate: 0 } })
194
+ setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 0 } })
195
195
  sr.ee.emit('errorAgg')
196
196
  sr.ee.emit('rumresp-sr', [true])
197
197
  await wait(1)
@@ -200,7 +200,7 @@ describe('Session Replay', () => {
200
200
  })
201
201
 
202
202
  test('An error AFTER rrweb import changes mode and starts harvester', async () => {
203
- setConfiguration(agentIdentifier, { session_replay: { errorSampleRate: 1, sampleRate: 0 } })
203
+ setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 0 } })
204
204
  sr.ee.emit('rumresp-sr', [true])
205
205
  await wait(1)
206
206
  expect(sr.mode).toEqual(MODE.ERROR)