@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
@@ -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
+ })
@@ -0,0 +1,29 @@
1
+ import { onLCP } from 'web-vitals'
2
+ import { VitalMetric } from './vital-metric'
3
+ import { VITAL_NAMES } from './constants'
4
+ import { initiallyHidden, isBrowserScope } from '../constants/runtime'
5
+ import { cleanURL } from '../url/clean-url'
6
+
7
+ export const largestContentfulPaint = new VitalMetric(VITAL_NAMES.LARGEST_CONTENTFUL_PAINT)
8
+
9
+ if (isBrowserScope) {
10
+ onLCP(({ value, entries }) => {
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
+ if (initiallyHidden || largestContentfulPaint.isValid) return
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
+ shouldAddConnectionAttributes: true
27
+ })
28
+ })
29
+ }
@@ -0,0 +1,94 @@
1
+ beforeEach(() => {
2
+ jest.resetModules()
3
+ jest.resetAllMocks()
4
+ jest.clearAllMocks()
5
+ })
6
+
7
+ const getFreshLCPImport = async (codeToRun) => {
8
+ const { largestContentfulPaint } = await import('./largest-contentful-paint')
9
+ codeToRun(largestContentfulPaint)
10
+ }
11
+
12
+ describe('lcp', () => {
13
+ test('reports lcp from web-vitals', (done) => {
14
+ getFreshLCPImport(metric => metric.subscribe(({ value, attrs }) => {
15
+ expect(value).toEqual(1)
16
+ expect(attrs).toMatchObject({
17
+ size: expect.any(Number),
18
+ eid: expect.any(String),
19
+ elUrl: expect.any(String),
20
+ elTag: expect.any(String)
21
+ })
22
+ done()
23
+ }))
24
+ })
25
+
26
+ test('does NOT report if not browser scoped', (done) => {
27
+ jest.doMock('../constants/runtime', () => ({
28
+ __esModule: true,
29
+ isBrowserScope: false
30
+ }))
31
+
32
+ getFreshLCPImport(metric => {
33
+ metric.subscribe(() => {
34
+ console.log('should not have reported...')
35
+ expect(1).toEqual(2)
36
+ })
37
+ setTimeout(done, 1000)
38
+ })
39
+ })
40
+
41
+ test('Does NOT report values if initiallyHidden', (done) => {
42
+ jest.doMock('../constants/runtime', () => ({
43
+ __esModule: true,
44
+ initiallyHidden: true,
45
+ isBrowserScope: true
46
+ }))
47
+
48
+ getFreshLCPImport(metric => {
49
+ metric.subscribe(() => {
50
+ console.log('should not have reported')
51
+ expect(1).toEqual(2)
52
+ })
53
+ setTimeout(done, 1000)
54
+ })
55
+ })
56
+
57
+ test('multiple subs get same value', done => {
58
+ jest.doMock('../constants/runtime', () => ({
59
+ __esModule: true,
60
+ isBrowserScope: true
61
+ }))
62
+ let sub1, sub2
63
+ getFreshLCPImport(metric => {
64
+ const remove1 = metric.subscribe(({ entries }) => {
65
+ sub1 ??= entries[0].id
66
+ if (sub1 === sub2) { remove1(); remove2(); done() }
67
+ })
68
+
69
+ const remove2 = metric.subscribe(({ entries }) => {
70
+ sub2 ??= entries[0].id
71
+ if (sub1 === sub2) { remove1(); remove2(); done() }
72
+ })
73
+ })
74
+ })
75
+
76
+ test('reports only once', (done) => {
77
+ jest.doMock('../constants/runtime', () => ({
78
+ __esModule: true,
79
+ initiallyHidden: false,
80
+ isBrowserScope: true
81
+ }))
82
+ let triggered = 0
83
+ getFreshLCPImport(metric => metric.subscribe(({ value }) => {
84
+ triggered++
85
+ expect(value).toEqual(1)
86
+ expect(triggered).toEqual(1)
87
+ setTimeout(() => {
88
+ expect(triggered).toEqual(1)
89
+ done()
90
+ }, 1000)
91
+ })
92
+ )
93
+ })
94
+ })
@@ -0,0 +1,52 @@
1
+ import { isBrowserScope } from '../constants/runtime'
2
+ import { subscribeToEOL } from '../unload/eol'
3
+ import { VITAL_NAMES } from './constants'
4
+ import { VitalMetric } from './vital-metric'
5
+
6
+ export const longTask = new VitalMetric(VITAL_NAMES.LONG_TASK)
7
+
8
+ if (isBrowserScope) {
9
+ const handleEntries = (entries) => {
10
+ entries.forEach(entry => {
11
+ longTask.update({
12
+ value: entry.duration,
13
+ entries: [entry],
14
+ attrs: {
15
+ ltFrame: entry.name, // MDN: the browsing context or frame that can be attributed to the long task
16
+ ltStart: entry.startTime, // MDN: a double representing the time (millisec) when the task started
17
+ ltCtr: entry.attribution[0].containerType, // MDN: type of frame container: 'iframe', 'embed', or 'object' ... but this can also be 'window',
18
+ ...(entry.attribution[0].containerType !== 'window' && {
19
+ ltCtrSrc: entry.attribution[0].containerSrc, // MDN: container's 'src' attribute
20
+ ltCtrId: entry.attribution[0].containerId, // MDN: container's 'id' attribute
21
+ ltCtrName: entry.attribution[0].containerName // MDN: container's 'name' attribute
22
+ })
23
+ }
24
+ })
25
+ })
26
+ }
27
+
28
+ let observer
29
+ try {
30
+ if (PerformanceObserver.supportedEntryTypes.includes('longtask')) {
31
+ observer = new PerformanceObserver((list) => {
32
+ // Delay by a microtask to workaround a bug in Safari where the
33
+ // callback is invoked immediately, rather than in a separate task.
34
+ // See: https://github.com/GoogleChrome/web-vitals/issues/277
35
+ Promise.resolve().then(() => {
36
+ handleEntries(list.getEntries())
37
+ })
38
+ })
39
+ observer.observe({ type: 'longtask', buffered: true })
40
+ }
41
+ } catch (e) {
42
+ // Do nothing.
43
+ }
44
+
45
+ if (observer) {
46
+ subscribeToEOL(() => {
47
+ handleEntries(observer.takeRecords())
48
+ }, true) // this bool is a temp arg under staged BFCache work that runs the func under the new page session logic -- tb removed w/ the feature flag later
49
+
50
+ /* No work needed on BFCache restore for long task. */
51
+ }
52
+ }
@@ -0,0 +1,122 @@
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({
11
+ getEntries: () => ([{
12
+ name: 'longtask',
13
+ duration: 1,
14
+ startTime: 1,
15
+ attribution: [{
16
+ containerType: 'object',
17
+ containerSrc: 'src',
18
+ containerId: 'id',
19
+ containerName: 'name'
20
+ }]
21
+ }])
22
+ })
23
+ setTimeout(callCb, 250)
24
+ }
25
+ setTimeout(callCb, 250)
26
+ },
27
+ disconnect: jest.fn()
28
+ }))
29
+ global.PerformanceObserver = mockPerformanceObserver
30
+ global.PerformanceObserver.supportedEntryTypes = ['longtask']
31
+ })
32
+
33
+ const getFreshLTImport = async (codeToRun) => {
34
+ const { longTask } = await import('./long-task')
35
+ codeToRun(longTask)
36
+ }
37
+
38
+ describe('lt', () => {
39
+ test('reports lt', (done) => {
40
+ getFreshLTImport(metric => metric.subscribe(({ value, attrs }) => {
41
+ expect(value).toEqual(1)
42
+ expect(attrs).toMatchObject({
43
+ ltFrame: 'longtask',
44
+ ltStart: 1,
45
+ ltCtr: 'object',
46
+ ltCtrSrc: 'src',
47
+ ltCtrId: 'id',
48
+ ltCtrName: 'name'
49
+ })
50
+ done()
51
+ }))
52
+ })
53
+
54
+ test('does NOT report if not browser scoped', (done) => {
55
+ jest.doMock('../constants/runtime', () => ({
56
+ __esModule: true,
57
+ isBrowserScope: false
58
+ }))
59
+
60
+ getFreshLTImport(metric => {
61
+ metric.subscribe(() => {
62
+ console.log('should not have reported...')
63
+ expect(1).toEqual(2)
64
+ })
65
+ setTimeout(done, 1000)
66
+ })
67
+ })
68
+
69
+ test('does NOT report if browser does not support longtask', (done) => {
70
+ jest.doMock('../constants/runtime', () => ({
71
+ __esModule: true,
72
+ isBrowserScope: true
73
+ }))
74
+
75
+ global.PerformanceObserver.supportedEntryTypes = ['paint']
76
+
77
+ getFreshLTImport(metric => {
78
+ metric.subscribe(() => {
79
+ console.log('should not have reported...')
80
+ expect(1).toEqual(2)
81
+ })
82
+ setTimeout(done, 1000)
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
+ getFreshLTImport(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 more than once', (done) => {
106
+ jest.doMock('../constants/runtime', () => ({
107
+ __esModule: true,
108
+ isBrowserScope: true
109
+ }))
110
+ let triggered = 0
111
+ getFreshLTImport(metric => metric.subscribe(({ value }) => {
112
+ triggered++
113
+ expect(value).toEqual(1)
114
+ expect(triggered).toEqual(1)
115
+ setTimeout(() => {
116
+ expect(triggered).toBeGreaterThanOrEqual(3)
117
+ done()
118
+ }, 1000)
119
+ })
120
+ )
121
+ })
122
+ })
@@ -0,0 +1,21 @@
1
+ import { globalScope, isBrowserScope, isiOS, offset } from '../constants/runtime'
2
+ import { VITAL_NAMES } from './constants'
3
+ import { VitalMetric } from './vital-metric'
4
+ import { onTTFB } from 'web-vitals'
5
+
6
+ export const timeToFirstByte = new VitalMetric(VITAL_NAMES.TIME_TO_FIRST_BYTE)
7
+
8
+ if (isBrowserScope && typeof PerformanceNavigationTiming !== 'undefined' && !isiOS) {
9
+ onTTFB(({ value, entries }) => {
10
+ if (!timeToFirstByte.isValid) timeToFirstByte.update({ value, entries })
11
+ })
12
+ } else {
13
+ if (!timeToFirstByte.isValid) {
14
+ const entry = {}
15
+ // convert real timestamps to relative timestamps to match web-vitals behavior
16
+ for (let key in globalScope?.performance?.timing || {}) entry[key] = Math.max(globalScope?.performance?.timing[key] - offset, 0)
17
+
18
+ // 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
+ }
21
+ }