@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
@@ -159,6 +159,10 @@ describe('_send', () => {
159
159
  jest.mocked(configModule.getRuntime).mockReturnValue({
160
160
  maxBytes: Infinity
161
161
  })
162
+ jest.mocked(configModule.getConfiguration).mockReturnValue({
163
+ ssl: undefined,
164
+ proxy: {}
165
+ })
162
166
 
163
167
  spec = {
164
168
  endpoint: faker.datatype.uuid(),
@@ -223,6 +227,19 @@ describe('_send', () => {
223
227
  })
224
228
  })
225
229
 
230
+ test('able to use and send to proxy when defined', () => {
231
+ jest.mocked(configModule.getConfiguration).mockReturnValue({ proxy: { beacon: 'some_other_string' } })
232
+ const result = harvestInstance._send(spec)
233
+
234
+ expect(result).toEqual(true)
235
+ expect(submitMethod).toHaveBeenCalledWith({
236
+ body: JSON.stringify(spec.payload.body),
237
+ headers: [{ key: 'content-type', value: 'text/plain' }],
238
+ sync: undefined,
239
+ url: expect.stringContaining(`https://some_other_string/${spec.endpoint}/1/${licenseKey}?`)
240
+ })
241
+ })
242
+
226
243
  test('should use the custom defined url', () => {
227
244
  spec.customUrl = faker.internet.url()
228
245
 
@@ -0,0 +1,19 @@
1
+ const continuouslyReportMetric = c => {
2
+ let count = 0
3
+ const vital = { value: 1, entries: [{ startTime: 1, name: 'name', size: 1, id: `id${++count}`, url: 'url', element: { tagName: 'tagName' } }], id: 'id' }
4
+ // report a new metric every quarter second
5
+ const callcb = () => {
6
+ setTimeout(() => {
7
+ c(vital)
8
+ callcb()
9
+ }, 250)
10
+ }
11
+ callcb()
12
+ }
13
+
14
+ export const onCLS = c => continuouslyReportMetric(c)
15
+ export const onFCP = c => continuouslyReportMetric(c)
16
+ export const onFID = c => continuouslyReportMetric(c)
17
+ export const onINP = c => continuouslyReportMetric(c)
18
+ export const onLCP = c => continuouslyReportMetric(c)
19
+ export const onTTFB = c => continuouslyReportMetric(c)
@@ -0,0 +1,10 @@
1
+ export const VITAL_NAMES = {
2
+ FIRST_PAINT: 'fp',
3
+ FIRST_CONTENTFUL_PAINT: 'fcp',
4
+ FIRST_INPUT_DELAY: 'fi',
5
+ LARGEST_CONTENTFUL_PAINT: 'lcp',
6
+ CUMULATIVE_LAYOUT_SHIFT: 'cls',
7
+ INTERACTION_TO_NEXT_PAINT: 'inp',
8
+ LONG_TASK: 'lt',
9
+ TIME_TO_FIRST_BYTE: 'ttfb'
10
+ }
@@ -0,0 +1,13 @@
1
+ import { onCLS } from 'web-vitals'
2
+ import { VITAL_NAMES } from './constants'
3
+ import { VitalMetric } from './vital-metric'
4
+ import { isBrowserScope } from '../constants/runtime'
5
+
6
+ export const cumulativeLayoutShift = new VitalMetric(VITAL_NAMES.CUMULATIVE_LAYOUT_SHIFT, (x) => x)
7
+
8
+ if (isBrowserScope) {
9
+ onCLS(({ value, entries }) => {
10
+ if (cumulativeLayoutShift.roundingMethod(value) === cumulativeLayoutShift.current.value) return
11
+ cumulativeLayoutShift.update({ value, entries })
12
+ }, { reportAllChanges: true })
13
+ }
@@ -0,0 +1,71 @@
1
+ afterEach(() => {
2
+ jest.resetModules()
3
+ jest.resetAllMocks()
4
+ jest.clearAllMocks()
5
+ })
6
+
7
+ const getFreshCLSImport = async (codeToRun) => {
8
+ const { cumulativeLayoutShift } = await import('./cumulative-layout-shift')
9
+ codeToRun(cumulativeLayoutShift)
10
+ }
11
+
12
+ describe('cls', () => {
13
+ test('reports cls', (done) => {
14
+ getFreshCLSImport(metric => {
15
+ metric.subscribe(({ value }) => {
16
+ expect(value).toEqual(1)
17
+ done()
18
+ })
19
+ })
20
+ })
21
+ test('does NOT report if not browser scoped', (done) => {
22
+ jest.doMock('../constants/runtime', () => ({
23
+ __esModule: true,
24
+ isBrowserScope: false
25
+ }))
26
+
27
+ getFreshCLSImport(metric => {
28
+ metric.subscribe(() => {
29
+ console.log('should not have reported...')
30
+ expect(1).toEqual(2)
31
+ })
32
+ setTimeout(done, 1000)
33
+ })
34
+ })
35
+ test('multiple subs get same value', done => {
36
+ jest.doMock('../constants/runtime', () => ({
37
+ __esModule: true,
38
+ isBrowserScope: true
39
+ }))
40
+ let sub1, sub2
41
+ getFreshCLSImport(metric => {
42
+ const remove1 = metric.subscribe(({ entries }) => {
43
+ sub1 ??= entries[0].id
44
+ if (sub1 === sub2) { remove1(); remove2(); done() }
45
+ })
46
+
47
+ const remove2 = metric.subscribe(({ entries }) => {
48
+ sub2 ??= entries[0].id
49
+ if (sub1 === sub2) { remove1(); remove2(); done() }
50
+ })
51
+ })
52
+ })
53
+ test('reports only new values', (done) => {
54
+ jest.doMock('../constants/runtime', () => ({
55
+ __esModule: true,
56
+ isBrowserScope: true
57
+ }))
58
+ let triggered = 0
59
+ getFreshCLSImport(metric => {
60
+ metric.subscribe(({ value }) => {
61
+ triggered++
62
+ expect(value).toEqual(1)
63
+ expect(triggered).toEqual(1)
64
+ setTimeout(() => {
65
+ expect(triggered).toEqual(1)
66
+ done()
67
+ }, 1000)
68
+ })
69
+ })
70
+ })
71
+ })
@@ -0,0 +1,31 @@
1
+ import { onFCP } from 'web-vitals'
2
+ // eslint-disable-next-line camelcase
3
+ import { iOSBelow16, initiallyHidden, isBrowserScope } from '../constants/runtime'
4
+ import { VITAL_NAMES } from './constants'
5
+ import { VitalMetric } from './vital-metric'
6
+
7
+ export const firstContentfulPaint = new VitalMetric(VITAL_NAMES.FIRST_CONTENTFUL_PAINT)
8
+
9
+ /* 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. */
10
+ if (isBrowserScope) {
11
+ // eslint-disable-next-line camelcase
12
+ if (iOSBelow16) {
13
+ try {
14
+ if (!initiallyHidden) { // see ios-version.js for detail on this following bug case; tldr: buffered flag doesn't work but getEntriesByType does
15
+ const paintEntries = performance.getEntriesByType('paint')
16
+ paintEntries.forEach(entry => {
17
+ if (entry.name === 'first-contentful-paint') {
18
+ firstContentfulPaint.update({ value: Math.floor(entry.startTime), entries: paintEntries })
19
+ }
20
+ })
21
+ }
22
+ } catch (e) {
23
+ // ignore
24
+ }
25
+ } else {
26
+ onFCP(({ value, entries }) => {
27
+ if (initiallyHidden || firstContentfulPaint.isValid) return
28
+ firstContentfulPaint.update({ value, entries })
29
+ })
30
+ }
31
+ }
@@ -0,0 +1,124 @@
1
+ beforeEach(() => {
2
+ jest.resetModules()
3
+ jest.resetAllMocks()
4
+ jest.clearAllMocks()
5
+ })
6
+
7
+ const getFreshFCPImport = async (codeToRun) => {
8
+ const { firstContentfulPaint } = await import('./first-contentful-paint')
9
+ codeToRun(firstContentfulPaint)
10
+ }
11
+
12
+ describe('fcp', () => {
13
+ test('reports fcp from web-vitals', (done) => {
14
+ getFreshFCPImport(firstContentfulPaint => firstContentfulPaint.subscribe(({ value }) => {
15
+ expect(value).toEqual(1)
16
+ done()
17
+ }))
18
+ })
19
+
20
+ test('reports fcp from paintEntries if ios<16', (done) => {
21
+ jest.doMock('../constants/runtime', () => ({
22
+ __esModule: true,
23
+ iOSBelow16: true,
24
+ initiallyHidden: false,
25
+ isBrowserScope: true
26
+ }))
27
+ global.performance.getEntriesByType = jest.fn(() => [{ name: 'first-contentful-paint', startTime: 1 }])
28
+
29
+ getFreshFCPImport(firstContentfulPaint => firstContentfulPaint.subscribe(({ value }) => {
30
+ expect(value).toEqual(1)
31
+ done()
32
+ }))
33
+ })
34
+
35
+ test('does NOT report if not browser scoped', (done) => {
36
+ jest.doMock('../constants/runtime', () => ({
37
+ __esModule: true,
38
+ isBrowserScope: false
39
+ }))
40
+
41
+ getFreshFCPImport(metric => {
42
+ metric.subscribe(({ value, attrs }) => {
43
+ console.log('should not have reported...')
44
+ expect(1).toEqual(2)
45
+ })
46
+ setTimeout(done, 1000)
47
+ })
48
+ })
49
+
50
+ test('Does NOT report values from paintEntries other than fcp', (done) => {
51
+ jest.doMock('../constants/runtime', () => ({
52
+ __esModule: true,
53
+ iOSBelow16: true,
54
+ initiallyHidden: false,
55
+ isBrowserScope: true
56
+ }))
57
+ global.performance.getEntriesByType = jest.fn(() => [{ name: 'other-timing-name', startTime: 1 }])
58
+
59
+ getFreshFCPImport(firstContentfulPaint => {
60
+ firstContentfulPaint.subscribe(() => {
61
+ console.log('should not have reported')
62
+ expect(1).toEqual(2)
63
+ })
64
+ setTimeout(done, 1000)
65
+ })
66
+ })
67
+
68
+ test('Does NOT report fcp from paintEntries if ios<16 && initiallyHidden', (done) => {
69
+ jest.doMock('../constants/runtime', () => ({
70
+ __esModule: true,
71
+ iOSBelow16: true,
72
+ initiallyHidden: true,
73
+ isBrowserScope: true
74
+ }))
75
+ global.performance.getEntriesByType = jest.fn(() => [{ name: 'first-contentful-paint', startTime: 1 }])
76
+
77
+ getFreshFCPImport(firstContentfulPaint => {
78
+ firstContentfulPaint.subscribe(() => {
79
+ console.log('should not have reported....')
80
+ expect(1).toEqual(2)
81
+ })
82
+ setTimeout(done, 2000)
83
+ })
84
+ })
85
+
86
+ test('multiple subs get same value', done => {
87
+ jest.doMock('../constants/runtime', () => ({
88
+ __esModule: true,
89
+ isBrowserScope: true
90
+ }))
91
+ let sub1, sub2
92
+ getFreshFCPImport(metric => {
93
+ const remove1 = metric.subscribe(({ entries }) => {
94
+ sub1 ??= entries[0].id
95
+ if (sub1 === sub2) { remove1(); remove2(); done() }
96
+ })
97
+
98
+ const remove2 = metric.subscribe(({ entries }) => {
99
+ sub2 ??= entries[0].id
100
+ if (sub1 === sub2) { remove1(); remove2(); done() }
101
+ })
102
+ })
103
+ })
104
+
105
+ test('reports only once', (done) => {
106
+ jest.doMock('../constants/runtime', () => ({
107
+ __esModule: true,
108
+ iOSBelow16: false,
109
+ initiallyHidden: false,
110
+ isBrowserScope: true
111
+ }))
112
+ let triggered = 0
113
+ getFreshFCPImport(firstContentfulPaint => firstContentfulPaint.subscribe(({ value }) => {
114
+ triggered++
115
+ expect(value).toEqual(1)
116
+ expect(triggered).toEqual(1)
117
+ setTimeout(() => {
118
+ expect(triggered).toEqual(1)
119
+ done()
120
+ }, 1000)
121
+ })
122
+ )
123
+ })
124
+ })
@@ -0,0 +1,20 @@
1
+ import { onFID } from 'web-vitals'
2
+ import { VitalMetric } from './vital-metric'
3
+ import { VITAL_NAMES } from './constants'
4
+ import { initiallyHidden, isBrowserScope } from '../constants/runtime'
5
+
6
+ export const firstInputDelay = new VitalMetric(VITAL_NAMES.FIRST_INPUT_DELAY)
7
+
8
+ if (isBrowserScope) {
9
+ onFID(({ value, entries }) => {
10
+ if (initiallyHidden || firstInputDelay.isValid || entries.length === 0) return
11
+
12
+ // 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
+ firstInputDelay.update({
14
+ value: entries[0].startTime,
15
+ entries,
16
+ attrs: { type: entries[0].name, fid: Math.round(value) },
17
+ shouldAddConnectionAttributes: true
18
+ })
19
+ })
20
+ }
@@ -0,0 +1,88 @@
1
+ beforeEach(() => {
2
+ jest.resetModules()
3
+ jest.resetAllMocks()
4
+ jest.clearAllMocks()
5
+ })
6
+
7
+ const getFreshFIDImport = async (codeToRun) => {
8
+ const { firstInputDelay } = await import('./first-input-delay')
9
+ codeToRun(firstInputDelay)
10
+ }
11
+
12
+ describe('fid', () => {
13
+ test('reports fcp from web-vitals', (done) => {
14
+ getFreshFIDImport(metric => metric.subscribe(({ value }) => {
15
+ expect(value).toEqual(1)
16
+ done()
17
+ }))
18
+ })
19
+
20
+ test('Does NOT report values if initiallyHidden', (done) => {
21
+ jest.doMock('../constants/runtime', () => ({
22
+ __esModule: true,
23
+ initiallyHidden: true,
24
+ isBrowserScope: true
25
+ }))
26
+
27
+ getFreshFIDImport(metric => {
28
+ metric.subscribe(() => {
29
+ console.log('should not have reported')
30
+ expect(1).toEqual(2)
31
+ })
32
+ setTimeout(done, 1000)
33
+ })
34
+ })
35
+
36
+ test('does NOT report if not browser scoped', (done) => {
37
+ jest.doMock('../constants/runtime', () => ({
38
+ __esModule: true,
39
+ isBrowserScope: false
40
+ }))
41
+
42
+ getFreshFIDImport(metric => {
43
+ metric.subscribe(() => {
44
+ console.log('should not have reported...')
45
+ expect(1).toEqual(2)
46
+ })
47
+ setTimeout(done, 1000)
48
+ })
49
+ })
50
+
51
+ test('multiple subs get same value', done => {
52
+ jest.doMock('../constants/runtime', () => ({
53
+ __esModule: true,
54
+ isBrowserScope: true
55
+ }))
56
+ let sub1, sub2
57
+ getFreshFIDImport(metric => {
58
+ const remove1 = metric.subscribe(({ entries }) => {
59
+ sub1 ??= entries[0].id
60
+ if (sub1 === sub2) { remove1(); remove2(); done() }
61
+ })
62
+
63
+ const remove2 = metric.subscribe(({ entries }) => {
64
+ sub2 ??= entries[0].id
65
+ if (sub1 === sub2) { remove1(); remove2(); done() }
66
+ })
67
+ })
68
+ })
69
+
70
+ test('reports only once', (done) => {
71
+ jest.doMock('../constants/runtime', () => ({
72
+ __esModule: true,
73
+ initiallyHidden: false,
74
+ isBrowserScope: true
75
+ }))
76
+ let triggered = 0
77
+ getFreshFIDImport(metric => metric.subscribe(({ value }) => {
78
+ triggered++
79
+ expect(value).toEqual(1)
80
+ expect(triggered).toEqual(1)
81
+ setTimeout(() => {
82
+ expect(triggered).toEqual(1)
83
+ done()
84
+ }, 1000)
85
+ })
86
+ )
87
+ })
88
+ })
@@ -1,28 +1,24 @@
1
- /**
2
- * Calls the `onReport` function when the 'first-paint' PerformancePaintTiming entry is observed.
3
- * The argument supplied is an object similar to the Metric type used by web-vitals library.
4
- *
5
- * @param {Function} onReport - callback that accepts a `metric` object as the single parameter
6
- */
7
- export const onFirstPaint = (onReport) => {
1
+ import { initiallyHidden, isBrowserScope } from '../constants/runtime'
2
+ import { VITAL_NAMES } from './constants'
3
+ import { VitalMetric } from './vital-metric'
4
+
5
+ export const firstPaint = new VitalMetric(VITAL_NAMES.FIRST_PAINT)
6
+
7
+ if (isBrowserScope) {
8
8
  const handleEntries = (entries) => {
9
9
  entries.forEach(entry => {
10
- if (entry.name === 'first-paint') {
10
+ if (entry.name === 'first-paint' && !firstPaint.isValid) {
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
- const metric = {
15
- name: 'FP',
16
- value: entry.startTime
17
- }
18
- onReport(metric)
14
+ firstPaint.update({ value: entry.startTime, entries })
19
15
  }
20
16
  })
21
17
  }
22
18
 
23
19
  let observer
24
20
  try {
25
- if (PerformanceObserver.supportedEntryTypes.includes('paint')) {
21
+ if (PerformanceObserver.supportedEntryTypes.includes('paint') && !initiallyHidden) {
26
22
  observer = new PerformanceObserver((list) => {
27
23
  // Delay by a microtask to workaround a bug in Safari where the
28
24
  // callback is invoked immediately, rather than in a separate task.
@@ -34,8 +30,6 @@ export const onFirstPaint = (onReport) => {
34
30
  observer.observe({ type: 'paint', buffered: true })
35
31
  }
36
32
  } catch (e) {
37
- // Do nothing.
33
+ // Do nothing.
38
34
  }
39
-
40
- /* BFCache restore not yet considered for first paint. See web-vitals onFCP for example. */
41
35
  }
@@ -0,0 +1,127 @@
1
+ beforeEach(() => {
2
+ jest.resetModules()
3
+ jest.resetAllMocks()
4
+ jest.clearAllMocks()
5
+
6
+ const mockPerformanceObserver = jest.fn(cb => ({
7
+ observe: () => {
8
+ const callCb = () => {
9
+ // eslint-disable-next-line
10
+ cb({getEntries: () => [{ name: 'first-paint', startTime: 1 }] })
11
+ setTimeout(callCb, 250)
12
+ }
13
+ setTimeout(callCb, 250)
14
+ },
15
+ disconnect: jest.fn()
16
+ }))
17
+ global.PerformanceObserver = mockPerformanceObserver
18
+ global.PerformanceObserver.supportedEntryTypes = ['paint']
19
+ })
20
+
21
+ const getFreshFPImport = async (codeToRun) => {
22
+ const { firstPaint } = await import('./first-paint')
23
+ codeToRun(firstPaint)
24
+ }
25
+
26
+ describe('fp', () => {
27
+ test('reports fp', (done) => {
28
+ getFreshFPImport(metric => metric.subscribe(({ value }) => {
29
+ expect(value).toEqual(1)
30
+ done()
31
+ }))
32
+ })
33
+
34
+ test('Does NOT report values if initiallyHidden', (done) => {
35
+ jest.doMock('../constants/runtime', () => ({
36
+ __esModule: true,
37
+ initiallyHidden: true,
38
+ isBrowserScope: true
39
+ }))
40
+
41
+ getFreshFPImport(metric => {
42
+ metric.subscribe(() => {
43
+ console.log('should not have reported')
44
+ expect(1).toEqual(2)
45
+ })
46
+ setTimeout(done, 1000)
47
+ })
48
+ })
49
+
50
+ test('does NOT report if not browser scoped', (done) => {
51
+ jest.doMock('../constants/runtime', () => ({
52
+ __esModule: true,
53
+ initiallyHidden: false,
54
+ isBrowserScope: false
55
+ }))
56
+
57
+ getFreshFPImport(metric => {
58
+ metric.subscribe(({ value, attrs }) => {
59
+ console.log('should not have reported...')
60
+ expect(1).toEqual(2)
61
+ })
62
+ setTimeout(done, 1000)
63
+ })
64
+ })
65
+
66
+ test('does NOT report other metrics', (done) => {
67
+ jest.doMock('../constants/runtime', () => ({
68
+ __esModule: true,
69
+ initiallyHidden: false,
70
+ isBrowserScope: true
71
+ }))
72
+
73
+ const mockPerformanceObserver = jest.fn(cb => ({
74
+ // eslint-disable-next-line
75
+ observe: () => cb({getEntries: () => [{ name: 'other-metric', startTime: 1 }] }),
76
+ disconnect: jest.fn()
77
+ }))
78
+ global.PerformanceObserver = mockPerformanceObserver
79
+ global.PerformanceObserver.supportedEntryTypes = ['paint']
80
+
81
+ getFreshFPImport(metric => {
82
+ metric.subscribe(() => {
83
+ console.log('should not have reported...')
84
+ expect(1).toEqual(2)
85
+ })
86
+ setTimeout(done, 1000)
87
+ })
88
+ })
89
+
90
+ test('multiple subs get same value', done => {
91
+ jest.doMock('../constants/runtime', () => ({
92
+ __esModule: true,
93
+ isBrowserScope: true
94
+ }))
95
+ let sub1, sub2
96
+ getFreshFPImport(metric => {
97
+ const remove1 = metric.subscribe(({ entries }) => {
98
+ sub1 ??= entries[0].id
99
+ if (sub1 === sub2) { remove1(); remove2(); done() }
100
+ })
101
+
102
+ const remove2 = metric.subscribe(({ entries }) => {
103
+ sub2 ??= entries[0].id
104
+ if (sub1 === sub2) { remove1(); remove2(); done() }
105
+ })
106
+ })
107
+ })
108
+
109
+ test('reports only once', (done) => {
110
+ jest.doMock('../constants/runtime', () => ({
111
+ __esModule: true,
112
+ initiallyHidden: false,
113
+ isBrowserScope: true
114
+ }))
115
+ let triggered = 0
116
+ getFreshFPImport(metric => metric.subscribe(({ value }) => {
117
+ triggered++
118
+ expect(value).toEqual(1)
119
+ expect(triggered).toEqual(1)
120
+ setTimeout(() => {
121
+ expect(triggered).toEqual(1)
122
+ done()
123
+ }, 1000)
124
+ })
125
+ )
126
+ })
127
+ })
@@ -0,0 +1,13 @@
1
+ import { onINP } from 'web-vitals'
2
+ import { VitalMetric } from './vital-metric'
3
+ import { VITAL_NAMES } from './constants'
4
+ import { isBrowserScope } from '../constants/runtime'
5
+
6
+ export const interactionToNextPaint = new VitalMetric(VITAL_NAMES.INTERACTION_TO_NEXT_PAINT)
7
+
8
+ if (isBrowserScope) {
9
+ /* Interaction-to-Next-Paint */
10
+ onINP(({ value, entries, id }) => {
11
+ interactionToNextPaint.update({ value, entries, attrs: { metricId: id } })
12
+ })
13
+ }
@@ -0,0 +1,74 @@
1
+ beforeEach(() => {
2
+ jest.resetModules()
3
+ jest.resetAllMocks()
4
+ jest.clearAllMocks()
5
+ })
6
+
7
+ const getFreshINPImport = async (codeToRun) => {
8
+ const { interactionToNextPaint } = await import('./interaction-to-next-paint')
9
+ codeToRun(interactionToNextPaint)
10
+ }
11
+
12
+ describe('inp', () => {
13
+ test('reports fcp from web-vitals', (done) => {
14
+ getFreshINPImport(metric => metric.subscribe(({ value, attrs }) => {
15
+ expect(value).toEqual(1)
16
+ expect(attrs.metricId).toEqual('id')
17
+ done()
18
+ }))
19
+ })
20
+
21
+ test('does NOT report if not browser scoped', (done) => {
22
+ jest.doMock('../constants/runtime', () => ({
23
+ __esModule: true,
24
+ isBrowserScope: false
25
+ }))
26
+
27
+ getFreshINPImport(metric => {
28
+ metric.subscribe(() => {
29
+ console.log('should not have reported...')
30
+ expect(1).toEqual(2)
31
+ })
32
+ setTimeout(done, 1000)
33
+ })
34
+ })
35
+
36
+ test('multiple subs get same value', done => {
37
+ jest.doMock('../constants/runtime', () => ({
38
+ __esModule: true,
39
+ isBrowserScope: true
40
+ }))
41
+ let sub1, sub2
42
+ getFreshINPImport(metric => {
43
+ const remove1 = metric.subscribe(({ entries }) => {
44
+ sub1 ??= entries[0].id
45
+ if (sub1 === sub2) { remove1(); remove2(); done() }
46
+ })
47
+
48
+ const remove2 = metric.subscribe(({ entries }) => {
49
+ sub2 ??= entries[0].id
50
+ if (sub1 === sub2) { remove1(); remove2(); done() }
51
+ })
52
+ })
53
+ })
54
+
55
+ test('reports more than once', (done) => {
56
+ jest.doMock('../constants/runtime', () => ({
57
+ __esModule: true,
58
+ isBrowserScope: true
59
+ }))
60
+ let triggered = 0
61
+ getFreshINPImport(metric => {
62
+ metric.subscribe(({ value }) => {
63
+ triggered++
64
+ expect(value).toEqual(1)
65
+ expect(triggered).toEqual(1)
66
+ })
67
+ setTimeout(() => {
68
+ // the metric emits every quarter second
69
+ expect(triggered).toBeGreaterThanOrEqual(3)
70
+ done()
71
+ }, 1000)
72
+ })
73
+ })
74
+ })