@newrelic/browser-agent 1.239.0 → 1.240.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 (167) 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 +6 -0
  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 +7 -0
  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 +27 -138
  25. package/dist/cjs/features/page_view_timing/instrument/index.js +0 -3
  26. package/dist/cjs/features/session_trace/aggregate/index.js +13 -1
  27. package/dist/cjs/features/spa/aggregate/index.js +4 -3
  28. package/dist/cjs/loaders/agent.js +3 -0
  29. package/dist/cjs/loaders/api/api.js +2 -0
  30. package/dist/cjs/loaders/api/apiAsync.js +4 -2
  31. package/dist/cjs/loaders/configure/configure.js +13 -1
  32. package/dist/cjs/loaders/configure/public-path.js +13 -0
  33. package/dist/cjs/loaders/configure/public-path.npm.js +10 -0
  34. package/dist/esm/cdn/pro.js +2 -1
  35. package/dist/esm/cdn/spa.js +2 -1
  36. package/dist/esm/common/config/state/init.js +6 -0
  37. package/dist/esm/common/constants/env.cdn.js +1 -1
  38. package/dist/esm/common/constants/env.npm.js +1 -1
  39. package/dist/esm/common/constants/runtime.js +5 -3
  40. package/dist/esm/common/harvest/harvest.js +6 -4
  41. package/dist/esm/common/vitals/constants.js +10 -0
  42. package/dist/esm/common/vitals/cumulative-layout-shift.js +20 -0
  43. package/dist/esm/common/vitals/first-contentful-paint.js +41 -0
  44. package/dist/esm/common/vitals/first-input-delay.js +25 -0
  45. package/dist/{cjs/features/page_view_timing → esm/common/vitals}/first-paint.js +12 -24
  46. package/dist/esm/common/vitals/interaction-to-next-paint.js +22 -0
  47. package/dist/esm/common/vitals/largest-contentful-paint.js +34 -0
  48. package/dist/esm/common/vitals/long-task.js +57 -0
  49. package/dist/esm/common/vitals/time-to-first-byte.js +29 -0
  50. package/dist/esm/common/vitals/vital-metric.js +64 -0
  51. package/dist/esm/features/ajax/aggregate/index.js +4 -1
  52. package/dist/esm/features/metrics/aggregate/index.js +8 -1
  53. package/dist/esm/features/page_view_event/aggregate/index.js +20 -42
  54. package/dist/esm/features/page_view_event/constants.js +1 -4
  55. package/dist/esm/features/page_view_event/instrument/index.js +0 -22
  56. package/dist/esm/features/page_view_timing/aggregate/index.js +28 -139
  57. package/dist/esm/features/page_view_timing/instrument/index.js +0 -3
  58. package/dist/esm/features/session_trace/aggregate/index.js +13 -1
  59. package/dist/esm/features/spa/aggregate/index.js +4 -3
  60. package/dist/esm/loaders/agent.js +2 -0
  61. package/dist/esm/loaders/api/api.js +2 -0
  62. package/dist/esm/loaders/api/apiAsync.js +5 -3
  63. package/dist/esm/loaders/configure/configure.js +13 -1
  64. package/dist/esm/loaders/configure/public-path.js +6 -0
  65. package/dist/esm/loaders/configure/public-path.npm.js +3 -0
  66. package/dist/types/common/config/state/init.d.ts.map +1 -1
  67. package/dist/types/common/constants/runtime.d.ts +3 -1
  68. package/dist/types/common/constants/runtime.d.ts.map +1 -1
  69. package/dist/types/common/harvest/harvest.d.ts +0 -1
  70. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  71. package/dist/types/common/vitals/constants.d.ts +11 -0
  72. package/dist/types/common/vitals/constants.d.ts.map +1 -0
  73. package/dist/types/common/vitals/cumulative-layout-shift.d.ts +3 -0
  74. package/dist/types/common/vitals/cumulative-layout-shift.d.ts.map +1 -0
  75. package/dist/types/common/vitals/first-contentful-paint.d.ts +3 -0
  76. package/dist/types/common/vitals/first-contentful-paint.d.ts.map +1 -0
  77. package/dist/types/common/vitals/first-input-delay.d.ts +3 -0
  78. package/dist/types/common/vitals/first-input-delay.d.ts.map +1 -0
  79. package/dist/types/common/vitals/first-paint.d.ts +3 -0
  80. package/dist/types/common/vitals/first-paint.d.ts.map +1 -0
  81. package/dist/types/common/vitals/interaction-to-next-paint.d.ts +3 -0
  82. package/dist/types/common/vitals/interaction-to-next-paint.d.ts.map +1 -0
  83. package/dist/types/common/vitals/largest-contentful-paint.d.ts +3 -0
  84. package/dist/types/common/vitals/largest-contentful-paint.d.ts.map +1 -0
  85. package/dist/types/common/vitals/long-task.d.ts +3 -0
  86. package/dist/types/common/vitals/long-task.d.ts.map +1 -0
  87. package/dist/types/common/vitals/time-to-first-byte.d.ts +3 -0
  88. package/dist/types/common/vitals/time-to-first-byte.d.ts.map +1 -0
  89. package/dist/types/common/vitals/vital-metric.d.ts +18 -0
  90. package/dist/types/common/vitals/vital-metric.d.ts.map +1 -0
  91. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  92. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  93. package/dist/types/features/page_view_event/aggregate/index.d.ts +3 -2
  94. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  95. package/dist/types/features/page_view_event/constants.d.ts +0 -3
  96. package/dist/types/features/page_view_event/constants.d.ts.map +1 -1
  97. package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
  98. package/dist/types/features/page_view_timing/aggregate/index.d.ts +1 -3
  99. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  100. package/dist/types/features/page_view_timing/instrument/index.d.ts.map +1 -1
  101. package/dist/types/features/session_trace/aggregate/index.d.ts +9 -1
  102. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  103. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  104. package/dist/types/loaders/agent.d.ts.map +1 -1
  105. package/dist/types/loaders/api/api.d.ts.map +1 -1
  106. package/dist/types/loaders/api/apiAsync.d.ts.map +1 -1
  107. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  108. package/dist/types/loaders/configure/public-path.d.ts +2 -0
  109. package/dist/types/loaders/configure/public-path.d.ts.map +1 -0
  110. package/dist/types/loaders/configure/public-path.npm.d.ts +2 -0
  111. package/dist/types/loaders/configure/public-path.npm.d.ts.map +1 -0
  112. package/package.json +1 -1
  113. package/src/cdn/pro.js +2 -0
  114. package/src/cdn/spa.js +2 -0
  115. package/src/common/config/state/init.js +4 -0
  116. package/src/common/constants/runtime.js +7 -3
  117. package/src/common/constants/runtime.test.js +8 -0
  118. package/src/common/harvest/harvest.js +6 -4
  119. package/src/common/harvest/harvest.test.js +17 -0
  120. package/src/common/vitals/__mocks__/web-vitals.js +19 -0
  121. package/src/common/vitals/constants.js +10 -0
  122. package/src/common/vitals/cumulative-layout-shift.js +13 -0
  123. package/src/common/vitals/cumulative-layout-shift.test.js +71 -0
  124. package/src/common/vitals/first-contentful-paint.js +31 -0
  125. package/src/common/vitals/first-contentful-paint.test.js +124 -0
  126. package/src/common/vitals/first-input-delay.js +20 -0
  127. package/src/common/vitals/first-input-delay.test.js +88 -0
  128. package/src/{features/page_view_timing → common/vitals}/first-paint.js +11 -17
  129. package/src/common/vitals/first-paint.test.js +127 -0
  130. package/src/common/vitals/interaction-to-next-paint.js +13 -0
  131. package/src/common/vitals/interaction-to-next-paint.test.js +74 -0
  132. package/src/common/vitals/largest-contentful-paint.js +29 -0
  133. package/src/common/vitals/largest-contentful-paint.test.js +94 -0
  134. package/src/common/vitals/long-task.js +52 -0
  135. package/src/common/vitals/long-task.test.js +122 -0
  136. package/src/common/vitals/time-to-first-byte.js +21 -0
  137. package/src/common/vitals/time-to-first-byte.test.js +147 -0
  138. package/src/common/vitals/vital-metric.js +60 -0
  139. package/src/common/vitals/vital-metric.test.js +171 -0
  140. package/src/features/ajax/aggregate/index.js +5 -1
  141. package/src/features/metrics/aggregate/index.js +6 -1
  142. package/src/features/page_view_event/aggregate/index.js +20 -43
  143. package/src/features/page_view_event/constants.js +0 -3
  144. package/src/features/page_view_event/instrument/index.js +0 -21
  145. package/src/features/page_view_timing/aggregate/index.component-test.js +86 -0
  146. package/src/features/page_view_timing/aggregate/index.js +24 -102
  147. package/src/features/page_view_timing/instrument/index.js +0 -3
  148. package/src/features/session_trace/aggregate/index.js +15 -1
  149. package/src/features/spa/aggregate/index.js +4 -3
  150. package/src/loaders/agent.js +2 -0
  151. package/src/loaders/api/api.js +2 -0
  152. package/src/loaders/api/apiAsync.js +5 -4
  153. package/src/loaders/configure/configure.js +15 -7
  154. package/src/loaders/configure/public-path.js +6 -0
  155. package/src/loaders/configure/public-path.npm.js +4 -0
  156. package/dist/cjs/common/metrics/paint-metrics.js +0 -13
  157. package/dist/cjs/features/page_view_timing/long-tasks.js +0 -75
  158. package/dist/esm/common/metrics/paint-metrics.js +0 -6
  159. package/dist/esm/features/page_view_timing/long-tasks.js +0 -69
  160. package/dist/types/common/metrics/paint-metrics.d.ts +0 -2
  161. package/dist/types/common/metrics/paint-metrics.d.ts.map +0 -1
  162. package/dist/types/features/page_view_timing/first-paint.d.ts +0 -2
  163. package/dist/types/features/page_view_timing/first-paint.d.ts.map +0 -1
  164. package/dist/types/features/page_view_timing/long-tasks.d.ts +0 -2
  165. package/dist/types/features/page_view_timing/long-tasks.d.ts.map +0 -1
  166. package/src/common/metrics/paint-metrics.js +0 -6
  167. package/src/features/page_view_timing/long-tasks.js +0 -60
@@ -0,0 +1,147 @@
1
+ beforeEach(() => {
2
+ jest.resetModules()
3
+ jest.resetAllMocks()
4
+ jest.clearAllMocks()
5
+ })
6
+
7
+ const getFreshTTFBImport = async (codeToRun) => {
8
+ const { timeToFirstByte } = await import('./time-to-first-byte')
9
+ codeToRun(timeToFirstByte)
10
+ }
11
+
12
+ describe('ttfb', () => {
13
+ test('reports ttfb from web-vitals', (done) => {
14
+ jest.doMock('../constants/runtime', () => ({
15
+ __esModule: true,
16
+ isiOS: false,
17
+ isBrowserScope: true
18
+ }))
19
+ global.PerformanceNavigationTiming = jest.fn()
20
+
21
+ getFreshTTFBImport(metric => metric.subscribe(({ value }) => {
22
+ expect(value).toEqual(1)
23
+ done()
24
+ }))
25
+ })
26
+
27
+ test('does NOT report if not browser scoped', (done) => {
28
+ jest.doMock('../constants/runtime', () => ({
29
+ __esModule: true,
30
+ isBrowserScope: false
31
+ }))
32
+
33
+ getFreshTTFBImport(metric => {
34
+ metric.subscribe(() => {
35
+ console.log('should not have reported...')
36
+ expect(1).toEqual(2)
37
+ })
38
+ setTimeout(done, 1000)
39
+ })
40
+ })
41
+
42
+ test('does NOT report ttfb from web-vitals if no PNT', (done) => {
43
+ jest.doMock('../constants/runtime', () => ({
44
+ __esModule: true,
45
+ isiOS: false,
46
+ isBrowserScope: true
47
+ }))
48
+ global.PerformanceNavigationTiming = undefined
49
+
50
+ getFreshTTFBImport(metric => {
51
+ metric.subscribe(() => {
52
+ console.log('should not have reported...')
53
+ expect(1).toEqual(2)
54
+ })
55
+ setTimeout(done, 1000)
56
+ })
57
+ })
58
+
59
+ test('does NOT report ttfb from web-vitals if is iOS', (done) => {
60
+ jest.doMock('../constants/runtime', () => ({
61
+ __esModule: true,
62
+ isiOS: true,
63
+ isBrowserScope: true
64
+ }))
65
+ global.PerformanceNavigationTiming = jest.fn()
66
+
67
+ getFreshTTFBImport(metric => {
68
+ metric.subscribe(() => {
69
+ console.log('should not have reported...')
70
+ expect(1).toEqual(2)
71
+ })
72
+ setTimeout(done, 1000)
73
+ })
74
+ })
75
+
76
+ test('reports from performance.timing if cant use web-vitals', (done) => {
77
+ jest.doMock('../constants/runtime', () => ({
78
+ __esModule: true,
79
+ isiOS: true,
80
+ globalScope: {
81
+ performance: {
82
+ timing: {
83
+ responseStart: 2
84
+ }
85
+ }
86
+ },
87
+ offset: 1,
88
+ isBrowserScope: true
89
+ }))
90
+ global.PerformanceNavigationTiming = undefined
91
+
92
+ getFreshTTFBImport(metric => {
93
+ metric.subscribe(({ value }) => {
94
+ expect(value).toEqual(1) // responseStart (2) - offset (1) === 1
95
+ done()
96
+ })
97
+ })
98
+ })
99
+
100
+ test('multiple subs get same value', done => {
101
+ jest.doMock('../constants/runtime', () => ({
102
+ __esModule: true,
103
+ isBrowserScope: true,
104
+ isiOS: false
105
+ }))
106
+ global.PerformanceNavigationTiming = jest.fn()
107
+ let sub1, sub2
108
+ getFreshTTFBImport(metric => {
109
+ const remove1 = metric.subscribe(({ entries }) => {
110
+ sub1 ??= entries[0].id
111
+ if (sub1 === sub2) { remove1(); remove2(); done() }
112
+ })
113
+
114
+ const remove2 = metric.subscribe(({ entries }) => {
115
+ sub2 ??= entries[0].id
116
+ if (sub1 === sub2) { remove1(); remove2(); done() }
117
+ })
118
+ })
119
+ })
120
+
121
+ test('reports only once', (done) => {
122
+ jest.doMock('../constants/runtime', () => ({
123
+ __esModule: true,
124
+ isiOS: true,
125
+ globalScope: {
126
+ performance: {
127
+ timing: {
128
+ responseStart: 2
129
+ }
130
+ }
131
+ },
132
+ offset: 1,
133
+ isBrowserScope: true
134
+ }))
135
+ let triggered = 0
136
+ getFreshTTFBImport(metric => metric.subscribe(({ value }) => {
137
+ triggered++
138
+ expect(value).toEqual(1)
139
+ expect(triggered).toEqual(1)
140
+ setTimeout(() => {
141
+ expect(triggered).toEqual(1)
142
+ done()
143
+ }, 1000)
144
+ })
145
+ )
146
+ })
147
+ })
@@ -0,0 +1,60 @@
1
+ export class VitalMetric {
2
+ #subscribers = new Set()
3
+ history = []
4
+
5
+ constructor (name, roundingMethod) {
6
+ this.name = name
7
+ this.attrs = {}
8
+ this.roundingMethod = typeof roundingMethod === 'function' ? roundingMethod : Math.floor
9
+ }
10
+
11
+ update ({ value, entries = [], attrs = {}, shouldAddConnectionAttributes = false }) {
12
+ if (value < 0) return
13
+ const state = {
14
+ value: this.roundingMethod(value),
15
+ name: this.name,
16
+ entries,
17
+ attrs
18
+ }
19
+ if (shouldAddConnectionAttributes) addConnectionAttributes(state.attrs)
20
+ this.history.push(state)
21
+ this.#subscribers.forEach(cb => {
22
+ try {
23
+ cb(this.current)
24
+ } catch (e) {
25
+ // ignore errors
26
+ }
27
+ })
28
+ }
29
+
30
+ get current () {
31
+ return this.history[this.history.length - 1] || {
32
+ value: undefined,
33
+ name: this.name,
34
+ entries: [],
35
+ attrs: {}
36
+ }
37
+ }
38
+
39
+ get isValid () {
40
+ return this.current.value >= 0
41
+ }
42
+
43
+ subscribe (callback, buffered = true) {
44
+ if (typeof callback !== 'function') return
45
+ this.#subscribers.add(callback)
46
+ // emit full history on subscription ("buffered" behavior)
47
+ if (this.isValid && !!buffered) this.history.forEach(state => { callback(state) })
48
+ return () => { this.#subscribers.delete(callback) }
49
+ }
50
+ }
51
+
52
+ function addConnectionAttributes (obj) {
53
+ var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection // to date, both window & worker shares the same support for connection
54
+ if (!connection) return
55
+
56
+ if (connection.type) obj['net-type'] = connection.type
57
+ if (connection.effectiveType) obj['net-etype'] = connection.effectiveType
58
+ if (connection.rtt) obj['net-rtt'] = connection.rtt
59
+ if (connection.downlink) obj['net-dlink'] = connection.downlink
60
+ }
@@ -0,0 +1,171 @@
1
+ import { VitalMetric } from './vital-metric'
2
+
3
+ let vitalMetric
4
+ beforeEach(() => {
5
+ vitalMetric = new VitalMetric('test')
6
+ })
7
+
8
+ afterEach(() => {
9
+ jest.resetAllMocks()
10
+ })
11
+
12
+ describe('vital-metric', () => {
13
+ test('default values', () => {
14
+ expect(vitalMetric).toMatchObject({
15
+ name: 'test',
16
+ history: []
17
+ })
18
+
19
+ expect(vitalMetric.current).toMatchObject({
20
+ name: 'test',
21
+ attrs: {},
22
+ entries: [],
23
+ value: undefined
24
+ })
25
+ })
26
+
27
+ test('update', () => {
28
+ let i = 0
29
+ vitalMetric.update({ value: i, entries: [{ test: i + 1 }], attrs: { test: i + 2 }, shouldAddConnectionAttributes: true })
30
+ expect(vitalMetric.current).toMatchObject({
31
+ value: i,
32
+ entries: [{ test: i + 1 }],
33
+ attrs: { test: i + 2 }
34
+ })
35
+
36
+ i++
37
+ vitalMetric.update({ value: i, entries: [{ test: i + 1 }], attrs: { test: i + 2 }, shouldAddConnectionAttributes: true })
38
+ expect(vitalMetric.current).toMatchObject({
39
+ value: i,
40
+ entries: [{ test: i + 1 }],
41
+ attrs: { test: i + 2 }
42
+ })
43
+ })
44
+
45
+ test('rounding', () => {
46
+ // default rounding
47
+ vitalMetric.update({ value: 1.234 })
48
+ expect(vitalMetric.current.value).toEqual(1)
49
+ // custom rounding
50
+ vitalMetric = new VitalMetric('test', (x) => x * 100)
51
+ vitalMetric.update({ value: 5 })
52
+ expect(vitalMetric.current.value).toEqual(500)
53
+ })
54
+
55
+ test('isValid', () => {
56
+ expect(vitalMetric.isValid).toEqual(false)
57
+
58
+ vitalMetric.update({ value: -1 })
59
+ expect(vitalMetric.isValid).toEqual(false)
60
+
61
+ vitalMetric.update({ value: 1 })
62
+ expect(vitalMetric.isValid).toEqual(true)
63
+ })
64
+
65
+ test('subscribers get updates when valid', (done) => {
66
+ vitalMetric.subscribe(({ value }) => {
67
+ expect(value).toEqual(1)
68
+ done()
69
+ })
70
+
71
+ vitalMetric.update({ value: 1 })
72
+ })
73
+
74
+ test('multiple subscribers get same update when valid', (done) => {
75
+ let sub1, sub2
76
+ let stop1 = vitalMetric.subscribe(({ entries }) => {
77
+ sub1 ??= entries[0].id
78
+ if (sub1 === sub2) { stop1(); stop2(); done() }
79
+ })
80
+
81
+ let stop2 = vitalMetric.subscribe(({ entries }) => {
82
+ sub2 ??= entries[0].id
83
+ if (sub1 === sub2) { stop1(); stop2(); done() }
84
+ })
85
+
86
+ vitalMetric.update({ value: 1, entries: [{ id: 'abcd' }] })
87
+ })
88
+
89
+ test('subscribers do not get updates when not valid', (done) => {
90
+ vitalMetric.subscribe(({ value }) => {
91
+ console.log(value)
92
+ console.log('should not have reached subscriber')
93
+ expect(1).toEqual(2)
94
+ })
95
+
96
+ vitalMetric.update({ value: -1 })
97
+ setTimeout(done, 1000)
98
+ })
99
+
100
+ test('subscribers get latest value immediately if already valid', (done) => {
101
+ vitalMetric.update({ value: 1 })
102
+
103
+ vitalMetric.subscribe(({ value }) => {
104
+ expect(value).toEqual(1)
105
+ done()
106
+ })
107
+ })
108
+
109
+ test('unsubscribe', (done) => {
110
+ const unsubscribe = vitalMetric.subscribe(({ value }) => {
111
+ console.log('should not have reached subscriber')
112
+ expect(1).toEqual(2)
113
+ })
114
+
115
+ unsubscribe()
116
+ vitalMetric.update({ value: 1 })
117
+ setTimeout(done, 1000)
118
+ })
119
+
120
+ test('addConnectionAttributes', () => {
121
+ global.navigator.connection = {}
122
+ vitalMetric.update({ value: 1, shouldAddConnectionAttributes: true })
123
+ expect(vitalMetric.current.attrs).toEqual(expect.objectContaining({}))
124
+
125
+ global.navigator.connection.type = 'type'
126
+ vitalMetric.update({ value: 1, shouldAddConnectionAttributes: true })
127
+ expect(vitalMetric.current.attrs).toEqual(expect.objectContaining({
128
+ 'net-type': 'type'
129
+ }))
130
+
131
+ global.navigator.connection.effectiveType = 'effectiveType'
132
+ vitalMetric.update({ value: 1, shouldAddConnectionAttributes: true })
133
+ expect(vitalMetric.current.attrs).toEqual(expect.objectContaining({
134
+ 'net-type': 'type',
135
+ 'net-etype': 'effectiveType'
136
+ }))
137
+
138
+ global.navigator.connection.rtt = 'rtt'
139
+ vitalMetric.update({ value: 1, shouldAddConnectionAttributes: true })
140
+ expect(vitalMetric.current.attrs).toEqual(expect.objectContaining({
141
+ 'net-type': 'type',
142
+ 'net-etype': 'effectiveType',
143
+ 'net-rtt': 'rtt'
144
+ }))
145
+
146
+ global.navigator.connection.downlink = 'downlink'
147
+ vitalMetric.update({ value: 1, shouldAddConnectionAttributes: true })
148
+ expect(vitalMetric.current.attrs).toEqual(expect.objectContaining({
149
+ 'net-type': 'type',
150
+ 'net-etype': 'effectiveType',
151
+ 'net-rtt': 'rtt',
152
+ 'net-dlink': 'downlink'
153
+ }))
154
+
155
+ global.navigator.connection = {
156
+ type: 'type',
157
+ effectiveType: 'effectiveType',
158
+ rtt: 'rtt',
159
+ downlink: 'downlink'
160
+ }
161
+ vitalMetric.update({ value: 1, shouldAddConnectionAttributes: true })
162
+
163
+ expect(vitalMetric.current.attrs).toEqual(expect.objectContaining({
164
+ 'net-type': 'type',
165
+ 'net-etype': 'effectiveType',
166
+ 'net-rtt': 'rtt',
167
+ 'net-dlink': 'downlink'
168
+ }))
169
+ global.navigator.connection = {}
170
+ })
171
+ })
@@ -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'
@@ -82,6 +82,11 @@ export class Aggregate extends AggregateBase {
82
82
  const rules = getRules(this.agentIdentifier)
83
83
  if (rules.length > 0) this.storeSupportabilityMetrics('Generic/Obfuscate/Detected')
84
84
  if (rules.length > 0 && !validateRules(rules)) this.storeSupportabilityMetrics('Generic/Obfuscate/Invalid')
85
+
86
+ // Check if proxy for either chunks or beacon is being used
87
+ const { proxy } = getConfiguration(this.agentIdentifier)
88
+ if (proxy.assets) this.storeSupportabilityMetrics('Config/AssetsUrl/Changed')
89
+ if (proxy.beacon) this.storeSupportabilityMetrics('Config/BeaconUrl/Changed')
85
90
  }
86
91
 
87
92
  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
+ })