@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.
- package/README.md +4 -0
- package/dist/cjs/cdn/pro.js +3 -2
- package/dist/cjs/cdn/spa.js +4 -3
- package/dist/cjs/common/config/state/init.js +6 -0
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/constants/runtime.js +9 -5
- package/dist/cjs/common/harvest/harvest.js +5 -3
- package/dist/cjs/common/vitals/constants.js +17 -0
- package/dist/cjs/common/vitals/cumulative-layout-shift.js +27 -0
- package/dist/cjs/common/vitals/first-contentful-paint.js +49 -0
- package/dist/cjs/common/vitals/first-input-delay.js +32 -0
- package/dist/{esm/features/page_view_timing → cjs/common/vitals}/first-paint.js +19 -17
- package/dist/cjs/common/vitals/interaction-to-next-paint.js +29 -0
- package/dist/cjs/common/vitals/largest-contentful-paint.js +41 -0
- package/dist/cjs/common/vitals/long-task.js +64 -0
- package/dist/cjs/common/vitals/time-to-first-byte.js +36 -0
- package/dist/cjs/common/vitals/vital-metric.js +71 -0
- package/dist/cjs/features/ajax/aggregate/index.js +4 -1
- package/dist/cjs/features/metrics/aggregate/index.js +7 -0
- package/dist/cjs/features/page_view_event/aggregate/index.js +18 -40
- package/dist/cjs/features/page_view_event/constants.js +2 -8
- package/dist/cjs/features/page_view_event/instrument/index.js +0 -22
- package/dist/cjs/features/page_view_timing/aggregate/index.js +27 -138
- package/dist/cjs/features/page_view_timing/instrument/index.js +0 -3
- package/dist/cjs/features/session_trace/aggregate/index.js +13 -1
- package/dist/cjs/features/spa/aggregate/index.js +4 -3
- package/dist/cjs/loaders/agent.js +3 -0
- package/dist/cjs/loaders/api/api.js +2 -0
- package/dist/cjs/loaders/api/apiAsync.js +4 -2
- package/dist/cjs/loaders/configure/configure.js +13 -1
- package/dist/cjs/loaders/configure/public-path.js +13 -0
- package/dist/cjs/loaders/configure/public-path.npm.js +10 -0
- package/dist/esm/cdn/pro.js +2 -1
- package/dist/esm/cdn/spa.js +2 -1
- package/dist/esm/common/config/state/init.js +6 -0
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/constants/runtime.js +5 -3
- package/dist/esm/common/harvest/harvest.js +6 -4
- package/dist/esm/common/vitals/constants.js +10 -0
- package/dist/esm/common/vitals/cumulative-layout-shift.js +20 -0
- package/dist/esm/common/vitals/first-contentful-paint.js +41 -0
- package/dist/esm/common/vitals/first-input-delay.js +25 -0
- package/dist/{cjs/features/page_view_timing → esm/common/vitals}/first-paint.js +12 -24
- package/dist/esm/common/vitals/interaction-to-next-paint.js +22 -0
- package/dist/esm/common/vitals/largest-contentful-paint.js +34 -0
- package/dist/esm/common/vitals/long-task.js +57 -0
- package/dist/esm/common/vitals/time-to-first-byte.js +29 -0
- package/dist/esm/common/vitals/vital-metric.js +64 -0
- package/dist/esm/features/ajax/aggregate/index.js +4 -1
- package/dist/esm/features/metrics/aggregate/index.js +8 -1
- package/dist/esm/features/page_view_event/aggregate/index.js +20 -42
- package/dist/esm/features/page_view_event/constants.js +1 -4
- package/dist/esm/features/page_view_event/instrument/index.js +0 -22
- package/dist/esm/features/page_view_timing/aggregate/index.js +28 -139
- package/dist/esm/features/page_view_timing/instrument/index.js +0 -3
- package/dist/esm/features/session_trace/aggregate/index.js +13 -1
- package/dist/esm/features/spa/aggregate/index.js +4 -3
- package/dist/esm/loaders/agent.js +2 -0
- package/dist/esm/loaders/api/api.js +2 -0
- package/dist/esm/loaders/api/apiAsync.js +5 -3
- package/dist/esm/loaders/configure/configure.js +13 -1
- package/dist/esm/loaders/configure/public-path.js +6 -0
- package/dist/esm/loaders/configure/public-path.npm.js +3 -0
- package/dist/types/common/config/state/init.d.ts.map +1 -1
- package/dist/types/common/constants/runtime.d.ts +3 -1
- package/dist/types/common/constants/runtime.d.ts.map +1 -1
- package/dist/types/common/harvest/harvest.d.ts +0 -1
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/common/vitals/constants.d.ts +11 -0
- package/dist/types/common/vitals/constants.d.ts.map +1 -0
- package/dist/types/common/vitals/cumulative-layout-shift.d.ts +3 -0
- package/dist/types/common/vitals/cumulative-layout-shift.d.ts.map +1 -0
- package/dist/types/common/vitals/first-contentful-paint.d.ts +3 -0
- package/dist/types/common/vitals/first-contentful-paint.d.ts.map +1 -0
- package/dist/types/common/vitals/first-input-delay.d.ts +3 -0
- package/dist/types/common/vitals/first-input-delay.d.ts.map +1 -0
- package/dist/types/common/vitals/first-paint.d.ts +3 -0
- package/dist/types/common/vitals/first-paint.d.ts.map +1 -0
- package/dist/types/common/vitals/interaction-to-next-paint.d.ts +3 -0
- package/dist/types/common/vitals/interaction-to-next-paint.d.ts.map +1 -0
- package/dist/types/common/vitals/largest-contentful-paint.d.ts +3 -0
- package/dist/types/common/vitals/largest-contentful-paint.d.ts.map +1 -0
- package/dist/types/common/vitals/long-task.d.ts +3 -0
- package/dist/types/common/vitals/long-task.d.ts.map +1 -0
- package/dist/types/common/vitals/time-to-first-byte.d.ts +3 -0
- package/dist/types/common/vitals/time-to-first-byte.d.ts.map +1 -0
- package/dist/types/common/vitals/vital-metric.d.ts +18 -0
- package/dist/types/common/vitals/vital-metric.d.ts.map +1 -0
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/index.d.ts +3 -2
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/constants.d.ts +0 -3
- package/dist/types/features/page_view_event/constants.d.ts.map +1 -1
- package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts +1 -3
- package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +9 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
- package/dist/types/loaders/agent.d.ts.map +1 -1
- package/dist/types/loaders/api/api.d.ts.map +1 -1
- package/dist/types/loaders/api/apiAsync.d.ts.map +1 -1
- package/dist/types/loaders/configure/configure.d.ts.map +1 -1
- package/dist/types/loaders/configure/public-path.d.ts +2 -0
- package/dist/types/loaders/configure/public-path.d.ts.map +1 -0
- package/dist/types/loaders/configure/public-path.npm.d.ts +2 -0
- package/dist/types/loaders/configure/public-path.npm.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cdn/pro.js +2 -0
- package/src/cdn/spa.js +2 -0
- package/src/common/config/state/init.js +4 -0
- package/src/common/constants/runtime.js +7 -3
- package/src/common/constants/runtime.test.js +8 -0
- package/src/common/harvest/harvest.js +6 -4
- package/src/common/harvest/harvest.test.js +17 -0
- package/src/common/vitals/__mocks__/web-vitals.js +19 -0
- package/src/common/vitals/constants.js +10 -0
- package/src/common/vitals/cumulative-layout-shift.js +13 -0
- package/src/common/vitals/cumulative-layout-shift.test.js +71 -0
- package/src/common/vitals/first-contentful-paint.js +31 -0
- package/src/common/vitals/first-contentful-paint.test.js +124 -0
- package/src/common/vitals/first-input-delay.js +20 -0
- package/src/common/vitals/first-input-delay.test.js +88 -0
- package/src/{features/page_view_timing → common/vitals}/first-paint.js +11 -17
- package/src/common/vitals/first-paint.test.js +127 -0
- package/src/common/vitals/interaction-to-next-paint.js +13 -0
- package/src/common/vitals/interaction-to-next-paint.test.js +74 -0
- package/src/common/vitals/largest-contentful-paint.js +29 -0
- package/src/common/vitals/largest-contentful-paint.test.js +94 -0
- package/src/common/vitals/long-task.js +52 -0
- package/src/common/vitals/long-task.test.js +122 -0
- package/src/common/vitals/time-to-first-byte.js +21 -0
- package/src/common/vitals/time-to-first-byte.test.js +147 -0
- package/src/common/vitals/vital-metric.js +60 -0
- package/src/common/vitals/vital-metric.test.js +171 -0
- package/src/features/ajax/aggregate/index.js +5 -1
- package/src/features/metrics/aggregate/index.js +6 -1
- package/src/features/page_view_event/aggregate/index.js +20 -43
- package/src/features/page_view_event/constants.js +0 -3
- package/src/features/page_view_event/instrument/index.js +0 -21
- package/src/features/page_view_timing/aggregate/index.component-test.js +86 -0
- package/src/features/page_view_timing/aggregate/index.js +24 -102
- package/src/features/page_view_timing/instrument/index.js +0 -3
- package/src/features/session_trace/aggregate/index.js +15 -1
- package/src/features/spa/aggregate/index.js +4 -3
- package/src/loaders/agent.js +2 -0
- package/src/loaders/api/api.js +2 -0
- package/src/loaders/api/apiAsync.js +5 -4
- package/src/loaders/configure/configure.js +15 -7
- package/src/loaders/configure/public-path.js +6 -0
- package/src/loaders/configure/public-path.npm.js +4 -0
- package/dist/cjs/common/metrics/paint-metrics.js +0 -13
- package/dist/cjs/features/page_view_timing/long-tasks.js +0 -75
- package/dist/esm/common/metrics/paint-metrics.js +0 -6
- package/dist/esm/features/page_view_timing/long-tasks.js +0 -69
- package/dist/types/common/metrics/paint-metrics.d.ts +0 -2
- package/dist/types/common/metrics/paint-metrics.d.ts.map +0 -1
- package/dist/types/features/page_view_timing/first-paint.d.ts +0 -2
- package/dist/types/features/page_view_timing/first-paint.d.ts.map +0 -1
- package/dist/types/features/page_view_timing/long-tasks.d.ts +0 -2
- package/dist/types/features/page_view_timing/long-tasks.d.ts.map +0 -1
- package/src/common/metrics/paint-metrics.js +0 -6
- package/src/features/page_view_timing/long-tasks.js +0 -60
|
@@ -1,28 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|