@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.
- 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 +25 -17
- 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 +17 -7
- 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 +36 -147
- package/dist/cjs/features/page_view_timing/instrument/index.js +0 -3
- package/dist/cjs/features/session_replay/aggregate/index.js +81 -35
- 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/browser-agent.js +2 -1
- 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 +25 -17
- 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 +18 -8
- 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 +37 -148
- package/dist/esm/features/page_view_timing/instrument/index.js +0 -3
- package/dist/esm/features/session_replay/aggregate/index.js +81 -35
- 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/browser-agent.js +2 -1
- 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_replay/aggregate/index.d.ts +16 -4
- package/dist/types/features/session_replay/aggregate/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/browser-agent.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 +2 -3
- package/src/cdn/pro.js +2 -0
- package/src/cdn/spa.js +2 -0
- package/src/common/config/state/init.js +21 -17
- package/src/common/constants/runtime.js +7 -3
- package/src/common/constants/runtime.test.js +8 -0
- package/src/common/harvest/harvest-scheduler.test.js +2 -2
- 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 +11 -4
- 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 +31 -108
- package/src/features/page_view_timing/instrument/index.js +0 -3
- package/src/features/session_replay/aggregate/index.component-test.js +10 -10
- package/src/features/session_replay/aggregate/index.js +62 -29
- 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/browser-agent.js +3 -1
- 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
|
@@ -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
|
-
|
|
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
|
+
})
|