@newrelic/browser-agent 1.254.1 → 1.256.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 (176) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cjs/common/config/state/runtime.js +2 -1
  3. package/dist/cjs/common/constants/env.cdn.js +2 -2
  4. package/dist/cjs/common/constants/env.npm.js +2 -2
  5. package/dist/cjs/common/constants/runtime.js +1 -1
  6. package/dist/cjs/common/context/shared-context.js +1 -1
  7. package/dist/cjs/common/harvest/harvest.js +2 -1
  8. package/dist/cjs/common/session/session-entity.js +2 -1
  9. package/dist/cjs/common/timer/interaction-timer.js +16 -2
  10. package/dist/cjs/common/timing/time-keeper.js +23 -19
  11. package/dist/cjs/common/vitals/cumulative-layout-shift.js +12 -5
  12. package/dist/cjs/common/vitals/first-contentful-paint.js +10 -6
  13. package/dist/cjs/common/vitals/first-input-delay.js +12 -10
  14. package/dist/cjs/common/vitals/first-paint.js +1 -2
  15. package/dist/cjs/common/vitals/interaction-to-next-paint.js +11 -7
  16. package/dist/cjs/common/vitals/largest-contentful-paint.js +19 -17
  17. package/dist/cjs/common/vitals/long-task.js +0 -1
  18. package/dist/cjs/common/vitals/time-to-first-byte.js +11 -6
  19. package/dist/cjs/common/vitals/vital-metric.js +1 -4
  20. package/dist/cjs/common/window/nreum.js +1 -1
  21. package/dist/cjs/features/ajax/aggregate/index.js +3 -2
  22. package/dist/cjs/features/ajax/instrument/index.js +1 -1
  23. package/dist/cjs/features/jserrors/aggregate/index.js +19 -8
  24. package/dist/cjs/features/jserrors/instrument/index.js +9 -4
  25. package/dist/cjs/features/page_action/aggregate/index.js +3 -2
  26. package/dist/cjs/features/page_view_event/aggregate/index.js +10 -5
  27. package/dist/cjs/features/page_view_timing/aggregate/index.js +21 -7
  28. package/dist/cjs/features/page_view_timing/instrument/index.js +1 -1
  29. package/dist/cjs/features/session_replay/aggregate/index.js +59 -35
  30. package/dist/cjs/features/session_replay/constants.js +2 -1
  31. package/dist/cjs/features/session_replay/instrument/index.js +9 -2
  32. package/dist/cjs/features/session_replay/shared/recorder.js +20 -4
  33. package/dist/cjs/features/session_replay/shared/utils.js +12 -0
  34. package/dist/cjs/features/session_trace/aggregate/index.js +20 -23
  35. package/dist/cjs/features/session_trace/instrument/index.js +1 -1
  36. package/dist/cjs/features/soft_navigations/aggregate/index.js +2 -2
  37. package/dist/cjs/features/soft_navigations/instrument/index.js +1 -1
  38. package/dist/cjs/features/spa/aggregate/index.js +19 -10
  39. package/dist/cjs/features/spa/instrument/index.js +1 -1
  40. package/dist/cjs/features/utils/feature-base.js +0 -2
  41. package/dist/cjs/loaders/agent-base.js +0 -2
  42. package/dist/cjs/loaders/agent.js +1 -1
  43. package/dist/cjs/loaders/api/api.js +8 -2
  44. package/dist/cjs/loaders/configure/configure.js +1 -0
  45. package/dist/cjs/loaders/micro-agent.js +4 -7
  46. package/dist/esm/common/config/state/runtime.js +2 -1
  47. package/dist/esm/common/constants/env.cdn.js +2 -2
  48. package/dist/esm/common/constants/env.npm.js +2 -2
  49. package/dist/esm/common/constants/runtime.js +1 -1
  50. package/dist/esm/common/context/shared-context.js +1 -1
  51. package/dist/esm/common/harvest/harvest.js +2 -1
  52. package/dist/esm/common/session/session-entity.js +2 -1
  53. package/dist/esm/common/timer/interaction-timer.js +16 -2
  54. package/dist/esm/common/timing/time-keeper.js +23 -20
  55. package/dist/esm/common/vitals/cumulative-layout-shift.js +11 -4
  56. package/dist/esm/common/vitals/first-contentful-paint.js +9 -5
  57. package/dist/esm/common/vitals/first-input-delay.js +11 -9
  58. package/dist/esm/common/vitals/first-paint.js +1 -2
  59. package/dist/esm/common/vitals/interaction-to-next-paint.js +10 -6
  60. package/dist/esm/common/vitals/largest-contentful-paint.js +18 -16
  61. package/dist/esm/common/vitals/long-task.js +0 -1
  62. package/dist/esm/common/vitals/time-to-first-byte.js +10 -5
  63. package/dist/esm/common/vitals/vital-metric.js +1 -4
  64. package/dist/esm/common/window/nreum.js +1 -1
  65. package/dist/esm/features/ajax/aggregate/index.js +3 -2
  66. package/dist/esm/features/ajax/instrument/index.js +1 -1
  67. package/dist/esm/features/jserrors/aggregate/index.js +19 -8
  68. package/dist/esm/features/jserrors/instrument/index.js +9 -4
  69. package/dist/esm/features/page_action/aggregate/index.js +3 -2
  70. package/dist/esm/features/page_view_event/aggregate/index.js +10 -5
  71. package/dist/esm/features/page_view_timing/aggregate/index.js +21 -7
  72. package/dist/esm/features/page_view_timing/instrument/index.js +1 -1
  73. package/dist/esm/features/session_replay/aggregate/index.js +59 -35
  74. package/dist/esm/features/session_replay/constants.js +2 -1
  75. package/dist/esm/features/session_replay/instrument/index.js +9 -2
  76. package/dist/esm/features/session_replay/shared/recorder.js +21 -5
  77. package/dist/esm/features/session_replay/shared/utils.js +11 -0
  78. package/dist/esm/features/session_trace/aggregate/index.js +20 -23
  79. package/dist/esm/features/session_trace/instrument/index.js +1 -1
  80. package/dist/esm/features/soft_navigations/aggregate/index.js +2 -2
  81. package/dist/esm/features/soft_navigations/instrument/index.js +1 -1
  82. package/dist/esm/features/spa/aggregate/index.js +19 -10
  83. package/dist/esm/features/spa/instrument/index.js +1 -1
  84. package/dist/esm/features/utils/feature-base.js +0 -2
  85. package/dist/esm/loaders/agent-base.js +0 -2
  86. package/dist/esm/loaders/agent.js +1 -1
  87. package/dist/esm/loaders/api/api.js +8 -2
  88. package/dist/esm/loaders/configure/configure.js +1 -0
  89. package/dist/esm/loaders/micro-agent.js +4 -7
  90. package/dist/types/common/config/state/runtime.d.ts.map +1 -1
  91. package/dist/types/common/constants/runtime.d.ts.map +1 -1
  92. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  93. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  94. package/dist/types/common/timer/interaction-timer.d.ts +2 -0
  95. package/dist/types/common/timer/interaction-timer.d.ts.map +1 -1
  96. package/dist/types/common/timing/time-keeper.d.ts +4 -9
  97. package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
  98. package/dist/types/common/vitals/vital-metric.d.ts +1 -2
  99. package/dist/types/common/vitals/vital-metric.d.ts.map +1 -1
  100. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  101. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  102. package/dist/types/features/jserrors/aggregate/index.d.ts +2 -1
  103. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  104. package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
  105. package/dist/types/features/page_action/aggregate/index.d.ts.map +1 -1
  106. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  107. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  108. package/dist/types/features/page_view_timing/instrument/index.d.ts.map +1 -1
  109. package/dist/types/features/session_replay/aggregate/index.d.ts +9 -2
  110. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  111. package/dist/types/features/session_replay/constants.d.ts +1 -0
  112. package/dist/types/features/session_replay/constants.d.ts.map +1 -1
  113. package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
  114. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  115. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  116. package/dist/types/features/session_replay/shared/utils.d.ts +8 -0
  117. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  118. package/dist/types/features/session_trace/aggregate/index.d.ts +2 -1
  119. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  120. package/dist/types/features/session_trace/instrument/index.d.ts.map +1 -1
  121. package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
  122. package/dist/types/features/spa/aggregate/index.d.ts +0 -2
  123. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  124. package/dist/types/features/utils/feature-base.d.ts +0 -1
  125. package/dist/types/features/utils/feature-base.d.ts.map +1 -1
  126. package/dist/types/loaders/agent-base.d.ts +0 -2
  127. package/dist/types/loaders/agent-base.d.ts.map +1 -1
  128. package/dist/types/loaders/api/api.d.ts.map +1 -1
  129. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  130. package/dist/types/loaders/micro-agent.d.ts.map +1 -1
  131. package/package.json +2 -2
  132. package/src/common/config/state/runtime.js +2 -1
  133. package/src/common/constants/runtime.js +1 -1
  134. package/src/common/context/__mocks__/shared-context.js +3 -0
  135. package/src/common/context/shared-context.js +1 -1
  136. package/src/common/harvest/harvest.js +2 -2
  137. package/src/common/session/session-entity.js +2 -1
  138. package/src/common/timer/interaction-timer.js +17 -2
  139. package/src/common/timing/__mocks__/time-keeper.js +2 -0
  140. package/src/common/timing/time-keeper.js +25 -22
  141. package/src/common/vitals/cumulative-layout-shift.js +10 -4
  142. package/src/common/vitals/first-contentful-paint.js +9 -4
  143. package/src/common/vitals/first-input-delay.js +11 -6
  144. package/src/common/vitals/first-paint.js +1 -1
  145. package/src/common/vitals/interaction-to-next-paint.js +10 -3
  146. package/src/common/vitals/largest-contentful-paint.js +19 -15
  147. package/src/common/vitals/long-task.js +0 -1
  148. package/src/common/vitals/time-to-first-byte.js +5 -4
  149. package/src/common/vitals/vital-metric.js +2 -4
  150. package/src/common/window/nreum.js +1 -1
  151. package/src/features/ajax/aggregate/index.js +3 -2
  152. package/src/features/ajax/instrument/index.js +1 -1
  153. package/src/features/jserrors/aggregate/index.js +18 -8
  154. package/src/features/jserrors/instrument/index.js +10 -5
  155. package/src/features/page_action/aggregate/index.js +3 -2
  156. package/src/features/page_view_event/aggregate/index.js +11 -5
  157. package/src/features/page_view_timing/aggregate/index.js +16 -6
  158. package/src/features/page_view_timing/instrument/index.js +1 -1
  159. package/src/features/session_replay/aggregate/index.js +53 -31
  160. package/src/features/session_replay/constants.js +2 -1
  161. package/src/features/session_replay/instrument/index.js +7 -2
  162. package/src/features/session_replay/shared/recorder.js +23 -5
  163. package/src/features/session_replay/shared/utils.js +12 -0
  164. package/src/features/session_trace/aggregate/index.js +19 -17
  165. package/src/features/session_trace/instrument/index.js +1 -1
  166. package/src/features/soft_navigations/aggregate/index.js +2 -2
  167. package/src/features/soft_navigations/instrument/index.js +1 -1
  168. package/src/features/spa/aggregate/index.js +19 -8
  169. package/src/features/spa/instrument/index.js +1 -1
  170. package/src/features/utils/feature-base.js +0 -3
  171. package/src/loaders/agent-base.js +0 -2
  172. package/src/loaders/agent.js +1 -1
  173. package/src/loaders/api/api.js +11 -2
  174. package/src/loaders/configure/configure.js +1 -0
  175. package/src/loaders/micro-agent.js +4 -6
  176. package/src/common/vitals/__mocks__/web-vitals.js +0 -19
@@ -1,13 +1,14 @@
1
- import { gosNREUM } from '../window/nreum'
2
- import { getRuntime } from '../config/config'
3
-
4
1
  /**
5
2
  * Class used to adjust the timestamp of harvested data to New Relic server time. This
6
3
  * is done by tracking the performance timings of the RUM call and applying a calculation
7
4
  * to the harvested data event offset time.
8
5
  */
9
6
  export class TimeKeeper {
10
- #agent
7
+ /**
8
+ * Represents the browser origin time.
9
+ * @type {number}
10
+ */
11
+ #originTime
11
12
 
12
13
  /**
13
14
  * Represents the browser origin time corrected to NR server time.
@@ -22,18 +23,26 @@ export class TimeKeeper {
22
23
  */
23
24
  #localTimeDiff
24
25
 
25
- constructor (agent) {
26
- this.#agent = agent
26
+ /**
27
+ * Represents whether the timekeeper is in a state that it can accurately convert
28
+ * timestamps.
29
+ * @type {number}
30
+ */
31
+ #ready = false
32
+
33
+ constructor () {
34
+ this.#originTime = Date.now() - performance.now()
27
35
  }
28
36
 
29
- static getTimeKeeperByAgentIdentifier (agentIdentifier) {
30
- const nr = gosNREUM()
31
- return Object.keys(nr?.initializedAgents || {}).indexOf(agentIdentifier) > -1
32
- ? nr.initializedAgents[agentIdentifier].timeKeeper
33
- : undefined
37
+ get ready () {
38
+ return this.#ready
34
39
  }
35
40
 
36
- get correctedPageOriginTime () {
41
+ get originTime () {
42
+ return this.#originTime
43
+ }
44
+
45
+ get correctedOriginTime () {
37
46
  return this.#correctedOriginTime
38
47
  }
39
48
 
@@ -54,18 +63,20 @@ export class TimeKeeper {
54
63
 
55
64
  // Corrected page origin time
56
65
  this.#correctedOriginTime = Math.floor(Date.parse(responseDateHeader) - serverOffset)
57
- this.#localTimeDiff = getRuntime(this.#agent.agentIdentifier).offset - this.#correctedOriginTime
66
+ this.#localTimeDiff = this.#originTime - this.#correctedOriginTime
58
67
 
59
68
  if (Number.isNaN(this.#correctedOriginTime)) {
60
69
  throw new Error('Date header invalid format.')
61
70
  }
71
+
72
+ this.#ready = true
62
73
  }
63
74
 
64
75
  /**
65
76
  * Converts a page origin relative time to an absolute timestamp
66
77
  * corrected to NR server time.
67
78
  * @param relativeTime {number} The relative time of the event in milliseconds
68
- * @returns {number} The correct timestamp as a unix/epoch timestamp value
79
+ * @returns {number} Corrected unix/epoch timestamp
69
80
  */
70
81
  convertRelativeTimestamp (relativeTime) {
71
82
  return this.#correctedOriginTime + relativeTime
@@ -79,12 +90,4 @@ export class TimeKeeper {
79
90
  correctAbsoluteTimestamp (timestamp) {
80
91
  return Math.floor(timestamp - this.#localTimeDiff)
81
92
  }
82
-
83
- /**
84
- * Returns the current time offset from page origin.
85
- * @return {number}
86
- */
87
- now () {
88
- return Math.floor(performance.now())
89
- }
90
93
  }
@@ -1,4 +1,4 @@
1
- import { onCLS } from 'web-vitals'
1
+ import { onCLS } from 'web-vitals/attribution'
2
2
  import { VITAL_NAMES } from './constants'
3
3
  import { VitalMetric } from './vital-metric'
4
4
  import { isBrowserScope } from '../constants/runtime'
@@ -6,8 +6,14 @@ import { isBrowserScope } from '../constants/runtime'
6
6
  export const cumulativeLayoutShift = new VitalMetric(VITAL_NAMES.CUMULATIVE_LAYOUT_SHIFT, (x) => x)
7
7
 
8
8
  if (isBrowserScope) {
9
- onCLS(({ value, entries }) => {
10
- if (cumulativeLayoutShift.roundingMethod(value) === cumulativeLayoutShift.current.value) return
11
- cumulativeLayoutShift.update({ value, entries })
9
+ onCLS(({ value, attribution, id }) => {
10
+ const attrs = {
11
+ metricId: id,
12
+ largestShiftTarget: attribution.largestShiftTarget,
13
+ largestShiftTime: attribution.largestShiftTime,
14
+ largestShiftValue: attribution.largestShiftValue,
15
+ loadState: attribution.loadState
16
+ }
17
+ cumulativeLayoutShift.update({ value, attrs })
12
18
  }, { reportAllChanges: true })
13
19
  }
@@ -1,4 +1,4 @@
1
- import { onFCP } from 'web-vitals'
1
+ import { onFCP } from 'web-vitals/attribution'
2
2
  // eslint-disable-next-line camelcase
3
3
  import { iOSBelow16, initiallyHidden, isBrowserScope } from '../constants/runtime'
4
4
  import { VITAL_NAMES } from './constants'
@@ -15,7 +15,7 @@ if (isBrowserScope) {
15
15
  const paintEntries = performance.getEntriesByType('paint')
16
16
  paintEntries.forEach(entry => {
17
17
  if (entry.name === 'first-contentful-paint') {
18
- firstContentfulPaint.update({ value: Math.floor(entry.startTime), entries: paintEntries })
18
+ firstContentfulPaint.update({ value: Math.floor(entry.startTime) })
19
19
  }
20
20
  })
21
21
  }
@@ -23,9 +23,14 @@ if (isBrowserScope) {
23
23
  // ignore
24
24
  }
25
25
  } else {
26
- onFCP(({ value, entries }) => {
26
+ onFCP(({ value, attribution }) => {
27
27
  if (initiallyHidden || firstContentfulPaint.isValid) return
28
- firstContentfulPaint.update({ value, entries })
28
+ const attrs = {
29
+ timeToFirstByte: attribution.timeToFirstByte,
30
+ firstByteToFCP: attribution.firstByteToFCP,
31
+ loadState: attribution.loadState
32
+ }
33
+ firstContentfulPaint.update({ value, attrs })
29
34
  })
30
35
  }
31
36
  }
@@ -1,4 +1,4 @@
1
- import { onFID } from 'web-vitals'
1
+ import { onFID } from 'web-vitals/attribution'
2
2
  import { VitalMetric } from './vital-metric'
3
3
  import { VITAL_NAMES } from './constants'
4
4
  import { initiallyHidden, isBrowserScope } from '../constants/runtime'
@@ -6,14 +6,19 @@ import { initiallyHidden, isBrowserScope } from '../constants/runtime'
6
6
  export const firstInputDelay = new VitalMetric(VITAL_NAMES.FIRST_INPUT_DELAY)
7
7
 
8
8
  if (isBrowserScope) {
9
- onFID(({ value, entries }) => {
10
- if (initiallyHidden || firstInputDelay.isValid || entries.length === 0) return
9
+ onFID(({ value, attribution }) => {
10
+ if (initiallyHidden || firstInputDelay.isValid) return
11
+ const attrs = {
12
+ type: attribution.eventType,
13
+ fid: Math.round(value),
14
+ eventTarget: attribution.eventTarget,
15
+ loadState: attribution.loadState
16
+ }
11
17
 
12
18
  // 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.
13
19
  firstInputDelay.update({
14
- value: entries[0].startTime,
15
- entries,
16
- attrs: { type: entries[0].name, fid: Math.round(value) }
20
+ value: attribution.eventTime,
21
+ attrs
17
22
  })
18
23
  })
19
24
  }
@@ -11,7 +11,7 @@ if (isBrowserScope) {
11
11
  observer.disconnect()
12
12
 
13
13
  /* Initial hidden state and pre-rendering not yet considered for first paint. See web-vitals onFCP for example. */
14
- firstPaint.update({ value: entry.startTime, entries })
14
+ firstPaint.update({ value: entry.startTime })
15
15
  }
16
16
  })
17
17
  }
@@ -1,4 +1,4 @@
1
- import { onINP } from 'web-vitals'
1
+ import { onINP } from 'web-vitals/attribution'
2
2
  import { VitalMetric } from './vital-metric'
3
3
  import { VITAL_NAMES } from './constants'
4
4
  import { isBrowserScope } from '../constants/runtime'
@@ -7,7 +7,14 @@ export const interactionToNextPaint = new VitalMetric(VITAL_NAMES.INTERACTION_TO
7
7
 
8
8
  if (isBrowserScope) {
9
9
  /* Interaction-to-Next-Paint */
10
- onINP(({ value, entries, id }) => {
11
- interactionToNextPaint.update({ value, entries, attrs: { metricId: id } })
10
+ onINP(({ value, attribution, id }) => {
11
+ const attrs = {
12
+ metricId: id,
13
+ eventTarget: attribution.eventTarget,
14
+ eventType: attribution.eventType,
15
+ eventTime: attribution.eventTime,
16
+ loadState: attribution.loadState
17
+ }
18
+ interactionToNextPaint.update({ value, attrs })
12
19
  })
13
20
  }
@@ -1,4 +1,4 @@
1
- import { onLCP } from 'web-vitals'
1
+ import { onLCP } from 'web-vitals/attribution'
2
2
  import { VitalMetric } from './vital-metric'
3
3
  import { VITAL_NAMES } from './constants'
4
4
  import { initiallyHidden, isBrowserScope } from '../constants/runtime'
@@ -7,22 +7,26 @@ import { cleanURL } from '../url/clean-url'
7
7
  export const largestContentfulPaint = new VitalMetric(VITAL_NAMES.LARGEST_CONTENTFUL_PAINT)
8
8
 
9
9
  if (isBrowserScope) {
10
- onLCP(({ value, entries }) => {
10
+ onLCP(({ value, attribution }) => {
11
11
  /* 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. */
12
12
  if (initiallyHidden || largestContentfulPaint.isValid) return
13
13
 
14
- 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...
15
- largestContentfulPaint.update({
16
- value,
17
- entries,
18
- ...(entries.length > 0 && {
19
- attrs: {
20
- size: lcpEntry.size,
21
- eid: lcpEntry.id,
22
- ...(!!lcpEntry.url && { elUrl: cleanURL(lcpEntry.url) }),
23
- ...(!!lcpEntry.element?.tagName && { elTag: lcpEntry.element.tagName })
24
- }
25
- })
26
- })
14
+ let attrs
15
+ const lcpEntry = attribution.lcpEntry
16
+ if (lcpEntry) {
17
+ attrs = {
18
+ size: lcpEntry.size,
19
+ eid: lcpEntry.id,
20
+ element: attribution.element,
21
+ timeToFirstByte: attribution.timeToFirstByte,
22
+ resourceLoadDelay: attribution.resourceLoadDelay,
23
+ resourceLoadTime: attribution.resourceLoadTime,
24
+ elementRenderDelay: attribution.elementRenderDelay
25
+ }
26
+ if (attribution.url) attrs.elUrl = cleanURL(attribution.url)
27
+ if (lcpEntry.element?.tagName) attrs.elTag = lcpEntry.element.tagName
28
+ }
29
+
30
+ largestContentfulPaint.update({ value, attrs })
27
31
  })
28
32
  }
@@ -10,7 +10,6 @@ if (isBrowserScope) {
10
10
  entries.forEach(entry => {
11
11
  longTask.update({
12
12
  value: entry.duration,
13
- entries: [entry],
14
13
  attrs: {
15
14
  ltFrame: entry.name, // MDN: the browsing context or frame that can be attributed to the long task
16
15
  ltStart: entry.startTime, // MDN: a double representing the time (millisec) when the task started
@@ -1,13 +1,14 @@
1
1
  import { globalScope, isBrowserScope, isiOS, offset } from '../constants/runtime'
2
2
  import { VITAL_NAMES } from './constants'
3
3
  import { VitalMetric } from './vital-metric'
4
- import { onTTFB } from 'web-vitals'
4
+ import { onTTFB } from 'web-vitals/attribution'
5
5
 
6
6
  export const timeToFirstByte = new VitalMetric(VITAL_NAMES.TIME_TO_FIRST_BYTE)
7
7
 
8
8
  if (isBrowserScope && typeof PerformanceNavigationTiming !== 'undefined' && !isiOS) {
9
- onTTFB(({ value, entries }) => {
10
- if (!timeToFirstByte.isValid) timeToFirstByte.update({ value, entries })
9
+ onTTFB(({ value, attribution }) => {
10
+ if (timeToFirstByte.isValid) return
11
+ timeToFirstByte.update({ value, attrs: { navigationEntry: attribution.navigationEntry } })
11
12
  })
12
13
  } else {
13
14
  if (!timeToFirstByte.isValid) {
@@ -16,6 +17,6 @@ if (isBrowserScope && typeof PerformanceNavigationTiming !== 'undefined' && !isi
16
17
  for (let key in globalScope?.performance?.timing || {}) entry[key] = Math.max(globalScope?.performance?.timing[key] - offset, 0)
17
18
 
18
19
  // ttfb is equiv to document's responseStart property in timing API --> https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming/responseStart
19
- timeToFirstByte.update({ value: entry.responseStart, entries: [entry] })
20
+ timeToFirstByte.update({ value: entry.responseStart, attrs: { navigationEntry: entry } })
20
21
  }
21
22
  }
@@ -8,19 +8,18 @@ export class VitalMetric {
8
8
  this.roundingMethod = typeof roundingMethod === 'function' ? roundingMethod : Math.floor
9
9
  }
10
10
 
11
- update ({ value, entries = [], attrs = {} }) {
11
+ update ({ value, attrs = {} }) {
12
12
  if (value < 0) return
13
13
  const state = {
14
14
  value: this.roundingMethod(value),
15
15
  name: this.name,
16
- entries,
17
16
  attrs
18
17
  }
19
18
 
20
19
  this.history.push(state)
21
20
  this.#subscribers.forEach(cb => {
22
21
  try {
23
- cb(this.current)
22
+ cb(state)
24
23
  } catch (e) {
25
24
  // ignore errors
26
25
  }
@@ -31,7 +30,6 @@ export class VitalMetric {
31
30
  return this.history[this.history.length - 1] || {
32
31
  value: undefined,
33
32
  name: this.name,
34
- entries: [],
35
33
  attrs: {}
36
34
  }
37
35
  }
@@ -1,5 +1,5 @@
1
- import { now } from '../timing/now'
2
1
  import { globalScope } from '../constants/runtime'
2
+ import { now } from '../timing/now'
3
3
 
4
4
  export const defaults = {
5
5
  beacon: 'bam.nr-data.net',
@@ -33,7 +33,8 @@ export class Aggregate extends AggregateBase {
33
33
  this.drain()
34
34
  })
35
35
 
36
- const denyList = getRuntime(agentIdentifier).denyList
36
+ const agentRuntime = getRuntime(agentIdentifier)
37
+ const denyList = agentRuntime.denyList
37
38
  setDenyList(denyList)
38
39
 
39
40
  let ajaxEvents = []
@@ -125,7 +126,7 @@ export class Aggregate extends AggregateBase {
125
126
  if (xhrContext.dt) {
126
127
  event.spanId = xhrContext.dt.spanId
127
128
  event.traceId = xhrContext.dt.traceId
128
- event.spanTimestamp = xhrContext.dt.timestamp
129
+ event.spanTimestamp = agentRuntime.timeKeeper.correctAbsoluteTimestamp(xhrContext.dt.timestamp)
129
130
  }
130
131
 
131
132
  // parsed from the AJAX body, looking for operationName param & parsing query for operationType
@@ -8,7 +8,6 @@ import { id } from '../../../common/ids/id'
8
8
  import { ffVersion, globalScope, isBrowserScope } from '../../../common/constants/runtime'
9
9
  import { dataSize } from '../../../common/util/data-size'
10
10
  import { eventListenerOpts } from '../../../common/event-listener/event-listener-opts'
11
- import { now } from '../../../common/timing/now'
12
11
  import { wrapFetch, wrapXhr } from '../../../common/wrap'
13
12
  import { parseUrl } from '../../../common/url/parse-url'
14
13
  import { DT } from './distributed-tracing'
@@ -17,6 +16,7 @@ import { InstrumentBase } from '../../utils/instrument-base'
17
16
  import { FEATURE_NAME } from '../constants'
18
17
  import { FEATURE_NAMES } from '../../../loaders/features/features'
19
18
  import { SUPPORTABILITY_METRIC } from '../../metrics/constants'
19
+ import { now } from '../../../common/timing/now'
20
20
 
21
21
  var handlers = ['load', 'error', 'abort', 'timeout']
22
22
  var handlersLen = handlers.length
@@ -14,7 +14,6 @@ import { stringify } from '../../../common/util/stringify'
14
14
  import { handle } from '../../../common/event-emitter/handle'
15
15
  import { mapOwn } from '../../../common/util/map-own'
16
16
  import { getInfo, getConfigurationValue, getRuntime } from '../../../common/config/config'
17
- import { now } from '../../../common/timing/now'
18
17
  import { globalScope } from '../../../common/constants/runtime'
19
18
 
20
19
  import { FEATURE_NAME } from '../constants'
@@ -22,6 +21,7 @@ import { FEATURE_NAMES } from '../../../loaders/features/features'
22
21
  import { AggregateBase } from '../../utils/aggregate-base'
23
22
  import { getNREUMInitializedAgent } from '../../../common/window/nreum'
24
23
  import { deregisterDrain } from '../../../common/drain/drain'
24
+ import { now } from '../../../common/timing/now'
25
25
 
26
26
  /**
27
27
  * @typedef {import('./compute-stack-trace.js').StackInfo} StackInfo
@@ -38,10 +38,13 @@ export class Aggregate extends AggregateBase {
38
38
  this.bufferedErrorsUnderSpa = {}
39
39
  this.currentBody = undefined
40
40
  this.errorOnPage = false
41
+ this.replayAborted = false
41
42
 
42
43
  // this will need to change to match whatever ee we use in the instrument
43
44
  this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved))
44
45
 
46
+ this.ee.on('REPLAY_ABORTED', () => { this.replayAborted = true })
47
+
45
48
  register('err', (...args) => this.storeError(...args), this.featureName, this.ee)
46
49
  register('ierr', (...args) => this.storeError(...args), this.featureName, this.ee)
47
50
  register('softNavFlush', (interactionId, wasFinished, softNavAttrs) =>
@@ -78,9 +81,16 @@ export class Aggregate extends AggregateBase {
78
81
  payload.qs.ri = releaseIds
79
82
  }
80
83
 
81
- if (body && body.err && body.err.length && !this.errorOnPage) {
82
- payload.qs.pve = '1'
83
- this.errorOnPage = true
84
+ if (body && body.err && body.err.length) {
85
+ if (this.replayAborted) {
86
+ body.err.forEach((e) => {
87
+ delete e.params?.hasReplay
88
+ })
89
+ }
90
+ if (!this.errorOnPage) {
91
+ payload.qs.pve = '1'
92
+ this.errorOnPage = true
93
+ }
84
94
  }
85
95
  return payload
86
96
  }
@@ -133,7 +143,7 @@ export class Aggregate extends AggregateBase {
133
143
  return canonicalStackString
134
144
  }
135
145
 
136
- storeError (err, time, internal, customAttributes) {
146
+ storeError (err, time, internal, customAttributes, hasReplay) {
137
147
  // are we in an interaction
138
148
  time = time || now()
139
149
  const agentRuntime = getRuntime(this.agentIdentifier)
@@ -173,7 +183,7 @@ export class Aggregate extends AggregateBase {
173
183
  if (!this.stackReported[bucketHash]) {
174
184
  this.stackReported[bucketHash] = true
175
185
  params.stack_trace = truncateSize(stackInfo.stackString)
176
- this.observedAt[bucketHash] = agentRuntime.offset + time
186
+ this.observedAt[bucketHash] = agentRuntime.timeKeeper.convertRelativeTimestamp(time)
177
187
  } else {
178
188
  params.browser_stack_hash = stringHashCode(stackInfo.stackString)
179
189
  }
@@ -189,8 +199,9 @@ export class Aggregate extends AggregateBase {
189
199
  this.pageviewReported[bucketHash] = true
190
200
  }
191
201
 
192
- if (agentRuntime?.session?.state?.sessionReplayMode) params.hasReplay = true
202
+ if (hasReplay && !this.replayAborted) params.hasReplay = hasReplay
193
203
  params.firstOccurrenceTimestamp = this.observedAt[bucketHash]
204
+ params.timestamp = this.observedAt[bucketHash]
194
205
 
195
206
  var type = internal ? 'ierr' : 'err'
196
207
  var newMetrics = { time }
@@ -198,7 +209,6 @@ export class Aggregate extends AggregateBase {
198
209
  // Trace sends the error in its payload, and both trace & replay simply listens for any error to occur.
199
210
  const jsErrorEvent = [type, bucketHash, params, newMetrics, customAttributes]
200
211
  handle('errorAgg', jsErrorEvent, undefined, FEATURE_NAMES.sessionTrace, this.ee)
201
- handle('errorAgg', jsErrorEvent, undefined, FEATURE_NAMES.sessionReplay, this.ee)
202
212
  // still send EE events for other features such as above, but stop this one from aggregating internal data
203
213
  if (this.blocked) return
204
214
 
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import { handle } from '../../../common/event-emitter/handle'
7
- import { now } from '../../../common/timing/now'
8
7
  import { InstrumentBase } from '../../utils/instrument-base'
9
8
  import { FEATURE_NAME } from '../constants'
10
9
  import { FEATURE_NAMES } from '../../../loaders/features/features'
@@ -12,11 +11,14 @@ import { globalScope } from '../../../common/constants/runtime'
12
11
  import { eventListenerOpts } from '../../../common/event-listener/event-listener-opts'
13
12
  import { stringify } from '../../../common/util/stringify'
14
13
  import { UncaughtError } from './uncaught-error'
14
+ import { now } from '../../../common/timing/now'
15
+ import { SR_EVENT_EMITTER_TYPES } from '../../session_replay/constants'
15
16
 
16
17
  export class Instrument extends InstrumentBase {
17
18
  static featureName = FEATURE_NAME
18
19
 
19
20
  #seenErrors = new Set()
21
+ #replayRunning = false
20
22
 
21
23
  constructor (agentIdentifier, aggregator, auto = true) {
22
24
  super(agentIdentifier, aggregator, FEATURE_NAME, auto)
@@ -36,13 +38,16 @@ export class Instrument extends InstrumentBase {
36
38
 
37
39
  this.ee.on('internal-error', (error) => {
38
40
  if (!this.abortHandler) return
39
- handle('ierr', [this.#castError(error), now(), true], undefined, FEATURE_NAMES.jserrors, this.ee)
41
+ handle('ierr', [this.#castError(error), now(), true, {}, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee)
42
+ })
43
+
44
+ this.ee.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, (isRunning) => {
45
+ this.#replayRunning = isRunning
40
46
  })
41
47
 
42
48
  globalScope.addEventListener('unhandledrejection', (promiseRejectionEvent) => {
43
49
  if (!this.abortHandler) return
44
-
45
- handle('err', [this.#castPromiseRejectionEvent(promiseRejectionEvent), now(), false, { unhandledPromiseRejection: 1 }], undefined, FEATURE_NAMES.jserrors, this.ee)
50
+ handle('err', [this.#castPromiseRejectionEvent(promiseRejectionEvent), now(), false, { unhandledPromiseRejection: 1 }, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee)
46
51
  }, eventListenerOpts(false, this.removeOnAbort?.signal))
47
52
 
48
53
  globalScope.addEventListener('error', (errorEvent) => {
@@ -57,7 +62,7 @@ export class Instrument extends InstrumentBase {
57
62
  return
58
63
  }
59
64
 
60
- handle('err', [this.#castErrorEvent(errorEvent), now()], undefined, FEATURE_NAMES.jserrors, this.ee)
65
+ handle('err', [this.#castErrorEvent(errorEvent), now(), false, {}, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee)
61
66
  }, eventListenerOpts(false, this.removeOnAbort?.signal))
62
67
 
63
68
  this.abortHandler = this.#abort // we also use this as a flag to denote that the feature is active or on and handling errors
@@ -86,14 +86,15 @@ export class Aggregate extends AggregateBase {
86
86
  height = window.document.documentElement.clientHeight
87
87
  }
88
88
 
89
+ const agentRuntime = getRuntime(this.agentIdentifier)
89
90
  var defaults = {
90
- timestamp: t + getRuntime(this.agentIdentifier).offset,
91
+ timestamp: agentRuntime.timeKeeper.convertRelativeTimestamp(t),
91
92
  timeSinceLoad: t / 1000,
92
93
  browserWidth: width,
93
94
  browserHeight: height,
94
95
  referrerUrl: this.referrerUrl,
95
96
  currentUrl: cleanURL('' + location),
96
- pageUrl: cleanURL(getRuntime(this.agentIdentifier).origin),
97
+ pageUrl: cleanURL(agentRuntime.origin),
97
98
  eventType: 'PageAction'
98
99
  }
99
100
 
@@ -15,6 +15,8 @@ import { drain } from '../../../common/drain/drain'
15
15
  import { FEATURE_NAMES } from '../../../loaders/features/features'
16
16
  import { handle } from '../../../common/event-emitter/handle'
17
17
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
18
+ import { now } from '../../../common/timing/now'
19
+ import { TimeKeeper } from '../../../common/timing/time-keeper'
18
20
 
19
21
  export class Aggregate extends AggregateBase {
20
22
  static featureName = CONSTANTS.FEATURE_NAME
@@ -26,8 +28,8 @@ export class Aggregate extends AggregateBase {
26
28
  this.firstByteToDomContent = 0 // our "dom processing" duration
27
29
 
28
30
  if (isBrowserScope) {
29
- timeToFirstByte.subscribe(({ value, entries }) => {
30
- const navEntry = entries[0]
31
+ timeToFirstByte.subscribe(({ value, attrs }) => {
32
+ const navEntry = attrs.navigationEntry
31
33
  this.timeToFirstByte = Math.max(value, this.timeToFirstByte)
32
34
  this.firstByteToWindowLoad = Math.max(Math.round(navEntry.loadEventEnd - this.timeToFirstByte), this.firstByteToWindowLoad) // our "frontend" duration
33
35
  this.firstByteToDomContent = Math.max(Math.round(navEntry.domContentLoadedEventEnd - this.timeToFirstByte), this.firstByteToDomContent) // our "dom processing" duration
@@ -100,13 +102,13 @@ export class Aggregate extends AggregateBase {
100
102
  queryParameters.fp = firstPaint.current.value
101
103
  queryParameters.fcp = firstContentfulPaint.current.value
102
104
 
103
- const rumStartTime = this.timeKeeper.now()
105
+ const rumStartTime = now()
104
106
  harvester.send({
105
107
  endpoint: 'rum',
106
108
  payload: { qs: queryParameters, body },
107
109
  opts: { needResponse: true, sendEmptyBody: true },
108
110
  cbFinished: ({ status, responseText, xhr }) => {
109
- const rumEndTime = this.timeKeeper.now()
111
+ const rumEndTime = now()
110
112
 
111
113
  if (status >= 400 || status === 0) {
112
114
  // Adding retry logic for the rum call will be a separate change
@@ -115,7 +117,11 @@ export class Aggregate extends AggregateBase {
115
117
  }
116
118
 
117
119
  try {
118
- this.timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime)
120
+ const timeKeeper = new TimeKeeper()
121
+ timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime)
122
+ if (!timeKeeper.ready) throw new Error('TimeKeeper not ready')
123
+
124
+ agentRuntime.timeKeeper = timeKeeper
119
125
  } catch (error) {
120
126
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['PVE/NRTime/Calculation/Failed'], undefined, FEATURE_NAMES.metrics, this.ee)
121
127
  drain(this.agentIdentifier, FEATURE_NAMES.metrics, true)
@@ -20,6 +20,8 @@ import { interactionToNextPaint } from '../../../common/vitals/interaction-to-ne
20
20
  import { largestContentfulPaint } from '../../../common/vitals/largest-contentful-paint'
21
21
  import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
22
22
  import { longTask } from '../../../common/vitals/long-task'
23
+ import { subscribeToVisibilityChange } from '../../../common/window/page-visibility'
24
+ import { VITAL_NAMES } from '../../../common/vitals/constants'
23
25
 
24
26
  export class Aggregate extends AggregateBase {
25
27
  static featureName = FEATURE_NAME
@@ -37,9 +39,6 @@ export class Aggregate extends AggregateBase {
37
39
 
38
40
  if (getConfigurationValue(this.agentIdentifier, 'page_view_timing.long_task') === true) longTask.subscribe(this.#handleVitalMetric)
39
41
 
40
- /* It's important that CWV api, like "onLCP", is called before this scheduler is initialized. The reason is because they listen to the same
41
- 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". */
42
-
43
42
  registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee)
44
43
  registerHandler('winPagehide', msTimestamp => this.recordPageUnload(msTimestamp), this.featureName, this.ee)
45
44
 
@@ -47,14 +46,24 @@ export class Aggregate extends AggregateBase {
47
46
  const harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.harvestTimeSeconds') || 30
48
47
 
49
48
  this.waitForFlags(([])).then(() => {
49
+ /* It's important that CWV api, like "onLCP", is called before the **scheduler** is initialized. The reason is because they listen to the same
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". */
50
51
  firstPaint.subscribe(this.#handleVitalMetric)
51
52
  firstContentfulPaint.subscribe(this.#handleVitalMetric)
52
53
  firstInputDelay.subscribe(this.#handleVitalMetric)
53
54
  largestContentfulPaint.subscribe(this.#handleVitalMetric)
54
55
  interactionToNextPaint.subscribe(this.#handleVitalMetric)
55
- timeToFirstByte.subscribe(({ entries }) => {
56
- this.addTiming('load', Math.round(entries[0].loadEventEnd))
56
+ timeToFirstByte.subscribe(({ attrs }) => {
57
+ this.addTiming('load', Math.round(attrs.navigationEntry.loadEventEnd))
57
58
  })
59
+ subscribeToVisibilityChange(() => {
60
+ /* Downstream, the event consumer interprets all timing node value as ms-unit and converts it to seconds via division by 1000. CLS is unitless so this normally is a problem.
61
+ bel.6 schema also doesn't support decimal values, of which cls within [0,1). However, the two nicely cancels out, and we can multiply cls by 1000 to both negate the division
62
+ and send an integer > 1. We effectively lose some precision down to 3 decimal places for this workaround. E.g. (real) 0.749132... -> 749.132...-> 749 -> 0.749 (final) */
63
+ const { name, value, attrs } = cumulativeLayoutShift.current
64
+ if (value === undefined) return
65
+ this.addTiming(name, value * 1000, attrs)
66
+ }, true) // CLS node should only reports on vis change rather than on every change
58
67
 
59
68
  const scheduler = new HarvestScheduler('events', {
60
69
  onFinished: (...args) => this.onHarvestFinished(...args),
@@ -102,8 +111,9 @@ export class Aggregate extends AggregateBase {
102
111
  Issue: Because NR 'pageHide' was only sent once with what is considered the "final" CLS value, in the case that 'pageHide' fires before 'load' happens, we incorrectly a final CLS of 0 for that page.
103
112
  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.
104
113
  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.
114
+ *cli Mar'24 update: CLS now emitted as its own timing node in addition to as-property under other nodes. The 'cls' property is unnecessary for cls nodes.
105
115
  */
106
- if (cumulativeLayoutShift.current.value >= 0) {
116
+ if (name !== VITAL_NAMES.CUMULATIVE_LAYOUT_SHIFT && cumulativeLayoutShift.current.value >= 0) {
107
117
  attrs.cls = cumulativeLayoutShift.current.value
108
118
  }
109
119
 
@@ -5,10 +5,10 @@
5
5
  import { handle } from '../../../common/event-emitter/handle'
6
6
  import { subscribeToVisibilityChange } from '../../../common/window/page-visibility'
7
7
  import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
8
- import { now } from '../../../common/timing/now'
9
8
  import { InstrumentBase } from '../../utils/instrument-base'
10
9
  import { FEATURE_NAME } from '../constants'
11
10
  import { isBrowserScope } from '../../../common/constants/runtime'
11
+ import { now } from '../../../common/timing/now'
12
12
 
13
13
  export class Instrument extends InstrumentBase {
14
14
  static featureName = FEATURE_NAME