@newrelic/browser-agent 1.254.1 → 1.256.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/CHANGELOG.md +25 -0
- package/dist/cjs/common/config/state/runtime.js +2 -1
- package/dist/cjs/common/constants/env.cdn.js +2 -2
- package/dist/cjs/common/constants/env.npm.js +2 -2
- package/dist/cjs/common/constants/runtime.js +1 -1
- package/dist/cjs/common/context/shared-context.js +1 -1
- package/dist/cjs/common/harvest/harvest.js +2 -1
- package/dist/cjs/common/session/session-entity.js +2 -1
- package/dist/cjs/common/timer/interaction-timer.js +16 -2
- package/dist/cjs/common/timing/time-keeper.js +23 -19
- package/dist/cjs/common/vitals/cumulative-layout-shift.js +12 -5
- package/dist/cjs/common/vitals/first-contentful-paint.js +10 -6
- package/dist/cjs/common/vitals/first-input-delay.js +12 -10
- package/dist/cjs/common/vitals/first-paint.js +1 -2
- package/dist/cjs/common/vitals/interaction-to-next-paint.js +11 -7
- package/dist/cjs/common/vitals/largest-contentful-paint.js +19 -17
- package/dist/cjs/common/vitals/long-task.js +0 -1
- package/dist/cjs/common/vitals/time-to-first-byte.js +11 -6
- package/dist/cjs/common/vitals/vital-metric.js +1 -4
- package/dist/cjs/common/window/nreum.js +1 -1
- package/dist/cjs/features/ajax/aggregate/index.js +3 -2
- package/dist/cjs/features/ajax/instrument/index.js +1 -1
- package/dist/cjs/features/jserrors/aggregate/index.js +19 -8
- package/dist/cjs/features/jserrors/instrument/index.js +9 -4
- package/dist/cjs/features/page_action/aggregate/index.js +3 -2
- package/dist/cjs/features/page_view_event/aggregate/index.js +10 -5
- package/dist/cjs/features/page_view_timing/aggregate/index.js +21 -7
- package/dist/cjs/features/page_view_timing/instrument/index.js +1 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +59 -35
- package/dist/cjs/features/session_replay/constants.js +2 -1
- package/dist/cjs/features/session_replay/instrument/index.js +9 -2
- package/dist/cjs/features/session_replay/shared/recorder.js +20 -4
- package/dist/cjs/features/session_replay/shared/utils.js +12 -0
- package/dist/cjs/features/session_trace/aggregate/index.js +20 -23
- package/dist/cjs/features/session_trace/instrument/index.js +1 -1
- package/dist/cjs/features/soft_navigations/aggregate/index.js +2 -2
- package/dist/cjs/features/soft_navigations/instrument/index.js +1 -1
- package/dist/cjs/features/spa/aggregate/index.js +19 -10
- package/dist/cjs/features/spa/instrument/index.js +1 -1
- package/dist/cjs/features/utils/feature-base.js +0 -2
- package/dist/cjs/loaders/agent-base.js +0 -2
- package/dist/cjs/loaders/agent.js +1 -1
- package/dist/cjs/loaders/api/api.js +8 -2
- package/dist/cjs/loaders/configure/configure.js +1 -0
- package/dist/cjs/loaders/micro-agent.js +4 -7
- package/dist/esm/common/config/state/runtime.js +2 -1
- package/dist/esm/common/constants/env.cdn.js +2 -2
- package/dist/esm/common/constants/env.npm.js +2 -2
- package/dist/esm/common/constants/runtime.js +1 -1
- package/dist/esm/common/context/shared-context.js +1 -1
- package/dist/esm/common/harvest/harvest.js +2 -1
- package/dist/esm/common/session/session-entity.js +2 -1
- package/dist/esm/common/timer/interaction-timer.js +16 -2
- package/dist/esm/common/timing/time-keeper.js +23 -20
- package/dist/esm/common/vitals/cumulative-layout-shift.js +11 -4
- package/dist/esm/common/vitals/first-contentful-paint.js +9 -5
- package/dist/esm/common/vitals/first-input-delay.js +11 -9
- package/dist/esm/common/vitals/first-paint.js +1 -2
- package/dist/esm/common/vitals/interaction-to-next-paint.js +10 -6
- package/dist/esm/common/vitals/largest-contentful-paint.js +18 -16
- package/dist/esm/common/vitals/long-task.js +0 -1
- package/dist/esm/common/vitals/time-to-first-byte.js +10 -5
- package/dist/esm/common/vitals/vital-metric.js +1 -4
- package/dist/esm/common/window/nreum.js +1 -1
- package/dist/esm/features/ajax/aggregate/index.js +3 -2
- package/dist/esm/features/ajax/instrument/index.js +1 -1
- package/dist/esm/features/jserrors/aggregate/index.js +19 -8
- package/dist/esm/features/jserrors/instrument/index.js +9 -4
- package/dist/esm/features/page_action/aggregate/index.js +3 -2
- package/dist/esm/features/page_view_event/aggregate/index.js +10 -5
- package/dist/esm/features/page_view_timing/aggregate/index.js +21 -7
- package/dist/esm/features/page_view_timing/instrument/index.js +1 -1
- package/dist/esm/features/session_replay/aggregate/index.js +59 -35
- package/dist/esm/features/session_replay/constants.js +2 -1
- package/dist/esm/features/session_replay/instrument/index.js +9 -2
- package/dist/esm/features/session_replay/shared/recorder.js +21 -5
- package/dist/esm/features/session_replay/shared/utils.js +11 -0
- package/dist/esm/features/session_trace/aggregate/index.js +20 -23
- package/dist/esm/features/session_trace/instrument/index.js +1 -1
- package/dist/esm/features/soft_navigations/aggregate/index.js +2 -2
- package/dist/esm/features/soft_navigations/instrument/index.js +1 -1
- package/dist/esm/features/spa/aggregate/index.js +19 -10
- package/dist/esm/features/spa/instrument/index.js +1 -1
- package/dist/esm/features/utils/feature-base.js +0 -2
- package/dist/esm/loaders/agent-base.js +0 -2
- package/dist/esm/loaders/agent.js +1 -1
- package/dist/esm/loaders/api/api.js +8 -2
- package/dist/esm/loaders/configure/configure.js +1 -0
- package/dist/esm/loaders/micro-agent.js +4 -7
- package/dist/types/common/config/state/runtime.d.ts.map +1 -1
- package/dist/types/common/constants/runtime.d.ts.map +1 -1
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/timer/interaction-timer.d.ts +2 -0
- package/dist/types/common/timer/interaction-timer.d.ts.map +1 -1
- package/dist/types/common/timing/time-keeper.d.ts +4 -9
- package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
- package/dist/types/common/vitals/vital-metric.d.ts +1 -2
- package/dist/types/common/vitals/vital-metric.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +2 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
- package/dist/types/features/page_action/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- 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 +9 -2
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +1 -0
- package/dist/types/features/session_replay/constants.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/utils.d.ts +8 -0
- package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +2 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_trace/instrument/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
- package/dist/types/features/spa/aggregate/index.d.ts +0 -2
- package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/utils/feature-base.d.ts +0 -1
- package/dist/types/features/utils/feature-base.d.ts.map +1 -1
- package/dist/types/loaders/agent-base.d.ts +0 -2
- package/dist/types/loaders/agent-base.d.ts.map +1 -1
- package/dist/types/loaders/api/api.d.ts.map +1 -1
- package/dist/types/loaders/configure/configure.d.ts.map +1 -1
- package/dist/types/loaders/micro-agent.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/config/state/runtime.js +2 -1
- package/src/common/constants/runtime.js +1 -1
- package/src/common/context/__mocks__/shared-context.js +3 -0
- package/src/common/context/shared-context.js +1 -1
- package/src/common/harvest/harvest.js +2 -2
- package/src/common/session/session-entity.js +2 -1
- package/src/common/timer/interaction-timer.js +17 -2
- package/src/common/timing/__mocks__/time-keeper.js +2 -0
- package/src/common/timing/time-keeper.js +25 -22
- package/src/common/vitals/cumulative-layout-shift.js +10 -4
- package/src/common/vitals/first-contentful-paint.js +9 -4
- package/src/common/vitals/first-input-delay.js +11 -6
- package/src/common/vitals/first-paint.js +1 -1
- package/src/common/vitals/interaction-to-next-paint.js +10 -3
- package/src/common/vitals/largest-contentful-paint.js +19 -15
- package/src/common/vitals/long-task.js +0 -1
- package/src/common/vitals/time-to-first-byte.js +5 -4
- package/src/common/vitals/vital-metric.js +2 -4
- package/src/common/window/nreum.js +1 -1
- package/src/features/ajax/aggregate/index.js +3 -2
- package/src/features/ajax/instrument/index.js +1 -1
- package/src/features/jserrors/aggregate/index.js +18 -8
- package/src/features/jserrors/instrument/index.js +10 -5
- package/src/features/page_action/aggregate/index.js +3 -2
- package/src/features/page_view_event/aggregate/index.js +11 -5
- package/src/features/page_view_timing/aggregate/index.js +16 -6
- package/src/features/page_view_timing/instrument/index.js +1 -1
- package/src/features/session_replay/aggregate/index.js +53 -31
- package/src/features/session_replay/constants.js +2 -1
- package/src/features/session_replay/instrument/index.js +7 -2
- package/src/features/session_replay/shared/recorder.js +23 -5
- package/src/features/session_replay/shared/utils.js +12 -0
- package/src/features/session_trace/aggregate/index.js +19 -17
- package/src/features/session_trace/instrument/index.js +1 -1
- package/src/features/soft_navigations/aggregate/index.js +2 -2
- package/src/features/soft_navigations/instrument/index.js +1 -1
- package/src/features/spa/aggregate/index.js +19 -8
- package/src/features/spa/instrument/index.js +1 -1
- package/src/features/utils/feature-base.js +0 -3
- package/src/loaders/agent-base.js +0 -2
- package/src/loaders/agent.js +1 -1
- package/src/loaders/api/api.js +11 -2
- package/src/loaders/configure/configure.js +1 -0
- package/src/loaders/micro-agent.js +4 -6
- package/src/common/vitals/__mocks__/web-vitals.js +0 -19
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { gosNREUM } from '../window/nreum'
|
|
2
|
-
import { getRuntime } from '../config/config'
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* Class used to adjust the timestamp of harvested data to New Relic server time. This
|
|
6
3
|
* is done by tracking the performance timings of the RUM call and applying a calculation
|
|
7
4
|
* to the harvested data event offset time.
|
|
8
5
|
*/
|
|
9
6
|
export class TimeKeeper {
|
|
10
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Represents the browser origin time.
|
|
9
|
+
* @type {number}
|
|
10
|
+
*/
|
|
11
|
+
#originTime
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Represents the browser origin time corrected to NR server time.
|
|
@@ -22,18 +23,26 @@ export class TimeKeeper {
|
|
|
22
23
|
*/
|
|
23
24
|
#localTimeDiff
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Represents whether the timekeeper is in a state that it can accurately convert
|
|
28
|
+
* timestamps.
|
|
29
|
+
* @type {number}
|
|
30
|
+
*/
|
|
31
|
+
#ready = false
|
|
32
|
+
|
|
33
|
+
constructor () {
|
|
34
|
+
this.#originTime = Date.now() - performance.now()
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return Object.keys(nr?.initializedAgents || {}).indexOf(agentIdentifier) > -1
|
|
32
|
-
? nr.initializedAgents[agentIdentifier].timeKeeper
|
|
33
|
-
: undefined
|
|
37
|
+
get ready () {
|
|
38
|
+
return this.#ready
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
get
|
|
41
|
+
get originTime () {
|
|
42
|
+
return this.#originTime
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get correctedOriginTime () {
|
|
37
46
|
return this.#correctedOriginTime
|
|
38
47
|
}
|
|
39
48
|
|
|
@@ -54,18 +63,20 @@ export class TimeKeeper {
|
|
|
54
63
|
|
|
55
64
|
// Corrected page origin time
|
|
56
65
|
this.#correctedOriginTime = Math.floor(Date.parse(responseDateHeader) - serverOffset)
|
|
57
|
-
this.#localTimeDiff =
|
|
66
|
+
this.#localTimeDiff = this.#originTime - this.#correctedOriginTime
|
|
58
67
|
|
|
59
68
|
if (Number.isNaN(this.#correctedOriginTime)) {
|
|
60
69
|
throw new Error('Date header invalid format.')
|
|
61
70
|
}
|
|
71
|
+
|
|
72
|
+
this.#ready = true
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
/**
|
|
65
76
|
* Converts a page origin relative time to an absolute timestamp
|
|
66
77
|
* corrected to NR server time.
|
|
67
78
|
* @param relativeTime {number} The relative time of the event in milliseconds
|
|
68
|
-
* @returns {number}
|
|
79
|
+
* @returns {number} Corrected unix/epoch timestamp
|
|
69
80
|
*/
|
|
70
81
|
convertRelativeTimestamp (relativeTime) {
|
|
71
82
|
return this.#correctedOriginTime + relativeTime
|
|
@@ -79,12 +90,4 @@ export class TimeKeeper {
|
|
|
79
90
|
correctAbsoluteTimestamp (timestamp) {
|
|
80
91
|
return Math.floor(timestamp - this.#localTimeDiff)
|
|
81
92
|
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Returns the current time offset from page origin.
|
|
85
|
-
* @return {number}
|
|
86
|
-
*/
|
|
87
|
-
now () {
|
|
88
|
-
return Math.floor(performance.now())
|
|
89
|
-
}
|
|
90
93
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onCLS } from 'web-vitals'
|
|
1
|
+
import { onCLS } from 'web-vitals/attribution'
|
|
2
2
|
import { VITAL_NAMES } from './constants'
|
|
3
3
|
import { VitalMetric } from './vital-metric'
|
|
4
4
|
import { isBrowserScope } from '../constants/runtime'
|
|
@@ -6,8 +6,14 @@ import { isBrowserScope } from '../constants/runtime'
|
|
|
6
6
|
export const cumulativeLayoutShift = new VitalMetric(VITAL_NAMES.CUMULATIVE_LAYOUT_SHIFT, (x) => x)
|
|
7
7
|
|
|
8
8
|
if (isBrowserScope) {
|
|
9
|
-
onCLS(({ value,
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
onCLS(({ value, attribution, id }) => {
|
|
10
|
+
const attrs = {
|
|
11
|
+
metricId: id,
|
|
12
|
+
largestShiftTarget: attribution.largestShiftTarget,
|
|
13
|
+
largestShiftTime: attribution.largestShiftTime,
|
|
14
|
+
largestShiftValue: attribution.largestShiftValue,
|
|
15
|
+
loadState: attribution.loadState
|
|
16
|
+
}
|
|
17
|
+
cumulativeLayoutShift.update({ value, attrs })
|
|
12
18
|
}, { reportAllChanges: true })
|
|
13
19
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onFCP } from 'web-vitals'
|
|
1
|
+
import { onFCP } from 'web-vitals/attribution'
|
|
2
2
|
// eslint-disable-next-line camelcase
|
|
3
3
|
import { iOSBelow16, initiallyHidden, isBrowserScope } from '../constants/runtime'
|
|
4
4
|
import { VITAL_NAMES } from './constants'
|
|
@@ -15,7 +15,7 @@ if (isBrowserScope) {
|
|
|
15
15
|
const paintEntries = performance.getEntriesByType('paint')
|
|
16
16
|
paintEntries.forEach(entry => {
|
|
17
17
|
if (entry.name === 'first-contentful-paint') {
|
|
18
|
-
firstContentfulPaint.update({ value: Math.floor(entry.startTime)
|
|
18
|
+
firstContentfulPaint.update({ value: Math.floor(entry.startTime) })
|
|
19
19
|
}
|
|
20
20
|
})
|
|
21
21
|
}
|
|
@@ -23,9 +23,14 @@ if (isBrowserScope) {
|
|
|
23
23
|
// ignore
|
|
24
24
|
}
|
|
25
25
|
} else {
|
|
26
|
-
onFCP(({ value,
|
|
26
|
+
onFCP(({ value, attribution }) => {
|
|
27
27
|
if (initiallyHidden || firstContentfulPaint.isValid) return
|
|
28
|
-
|
|
28
|
+
const attrs = {
|
|
29
|
+
timeToFirstByte: attribution.timeToFirstByte,
|
|
30
|
+
firstByteToFCP: attribution.firstByteToFCP,
|
|
31
|
+
loadState: attribution.loadState
|
|
32
|
+
}
|
|
33
|
+
firstContentfulPaint.update({ value, attrs })
|
|
29
34
|
})
|
|
30
35
|
}
|
|
31
36
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onFID } from 'web-vitals'
|
|
1
|
+
import { onFID } from 'web-vitals/attribution'
|
|
2
2
|
import { VitalMetric } from './vital-metric'
|
|
3
3
|
import { VITAL_NAMES } from './constants'
|
|
4
4
|
import { initiallyHidden, isBrowserScope } from '../constants/runtime'
|
|
@@ -6,14 +6,19 @@ import { initiallyHidden, isBrowserScope } from '../constants/runtime'
|
|
|
6
6
|
export const firstInputDelay = new VitalMetric(VITAL_NAMES.FIRST_INPUT_DELAY)
|
|
7
7
|
|
|
8
8
|
if (isBrowserScope) {
|
|
9
|
-
onFID(({ value,
|
|
10
|
-
if (initiallyHidden || firstInputDelay.isValid
|
|
9
|
+
onFID(({ value, attribution }) => {
|
|
10
|
+
if (initiallyHidden || firstInputDelay.isValid) return
|
|
11
|
+
const attrs = {
|
|
12
|
+
type: attribution.eventType,
|
|
13
|
+
fid: Math.round(value),
|
|
14
|
+
eventTarget: attribution.eventTarget,
|
|
15
|
+
loadState: attribution.loadState
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
// 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
19
|
firstInputDelay.update({
|
|
14
|
-
value:
|
|
15
|
-
|
|
16
|
-
attrs: { type: entries[0].name, fid: Math.round(value) }
|
|
20
|
+
value: attribution.eventTime,
|
|
21
|
+
attrs
|
|
17
22
|
})
|
|
18
23
|
})
|
|
19
24
|
}
|
|
@@ -11,7 +11,7 @@ if (isBrowserScope) {
|
|
|
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
|
-
firstPaint.update({ value: entry.startTime
|
|
14
|
+
firstPaint.update({ value: entry.startTime })
|
|
15
15
|
}
|
|
16
16
|
})
|
|
17
17
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onINP } from 'web-vitals'
|
|
1
|
+
import { onINP } from 'web-vitals/attribution'
|
|
2
2
|
import { VitalMetric } from './vital-metric'
|
|
3
3
|
import { VITAL_NAMES } from './constants'
|
|
4
4
|
import { isBrowserScope } from '../constants/runtime'
|
|
@@ -7,7 +7,14 @@ export const interactionToNextPaint = new VitalMetric(VITAL_NAMES.INTERACTION_TO
|
|
|
7
7
|
|
|
8
8
|
if (isBrowserScope) {
|
|
9
9
|
/* Interaction-to-Next-Paint */
|
|
10
|
-
onINP(({ value,
|
|
11
|
-
|
|
10
|
+
onINP(({ value, attribution, id }) => {
|
|
11
|
+
const attrs = {
|
|
12
|
+
metricId: id,
|
|
13
|
+
eventTarget: attribution.eventTarget,
|
|
14
|
+
eventType: attribution.eventType,
|
|
15
|
+
eventTime: attribution.eventTime,
|
|
16
|
+
loadState: attribution.loadState
|
|
17
|
+
}
|
|
18
|
+
interactionToNextPaint.update({ value, attrs })
|
|
12
19
|
})
|
|
13
20
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onLCP } from 'web-vitals'
|
|
1
|
+
import { onLCP } from 'web-vitals/attribution'
|
|
2
2
|
import { VitalMetric } from './vital-metric'
|
|
3
3
|
import { VITAL_NAMES } from './constants'
|
|
4
4
|
import { initiallyHidden, isBrowserScope } from '../constants/runtime'
|
|
@@ -7,22 +7,26 @@ import { cleanURL } from '../url/clean-url'
|
|
|
7
7
|
export const largestContentfulPaint = new VitalMetric(VITAL_NAMES.LARGEST_CONTENTFUL_PAINT)
|
|
8
8
|
|
|
9
9
|
if (isBrowserScope) {
|
|
10
|
-
onLCP(({ value,
|
|
10
|
+
onLCP(({ value, attribution }) => {
|
|
11
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
12
|
if (initiallyHidden || largestContentfulPaint.isValid) return
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
14
|
+
let attrs
|
|
15
|
+
const lcpEntry = attribution.lcpEntry
|
|
16
|
+
if (lcpEntry) {
|
|
17
|
+
attrs = {
|
|
18
|
+
size: lcpEntry.size,
|
|
19
|
+
eid: lcpEntry.id,
|
|
20
|
+
element: attribution.element,
|
|
21
|
+
timeToFirstByte: attribution.timeToFirstByte,
|
|
22
|
+
resourceLoadDelay: attribution.resourceLoadDelay,
|
|
23
|
+
resourceLoadTime: attribution.resourceLoadTime,
|
|
24
|
+
elementRenderDelay: attribution.elementRenderDelay
|
|
25
|
+
}
|
|
26
|
+
if (attribution.url) attrs.elUrl = cleanURL(attribution.url)
|
|
27
|
+
if (lcpEntry.element?.tagName) attrs.elTag = lcpEntry.element.tagName
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
largestContentfulPaint.update({ value, attrs })
|
|
27
31
|
})
|
|
28
32
|
}
|
|
@@ -10,7 +10,6 @@ if (isBrowserScope) {
|
|
|
10
10
|
entries.forEach(entry => {
|
|
11
11
|
longTask.update({
|
|
12
12
|
value: entry.duration,
|
|
13
|
-
entries: [entry],
|
|
14
13
|
attrs: {
|
|
15
14
|
ltFrame: entry.name, // MDN: the browsing context or frame that can be attributed to the long task
|
|
16
15
|
ltStart: entry.startTime, // MDN: a double representing the time (millisec) when the task started
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { globalScope, isBrowserScope, isiOS, offset } from '../constants/runtime'
|
|
2
2
|
import { VITAL_NAMES } from './constants'
|
|
3
3
|
import { VitalMetric } from './vital-metric'
|
|
4
|
-
import { onTTFB } from 'web-vitals'
|
|
4
|
+
import { onTTFB } from 'web-vitals/attribution'
|
|
5
5
|
|
|
6
6
|
export const timeToFirstByte = new VitalMetric(VITAL_NAMES.TIME_TO_FIRST_BYTE)
|
|
7
7
|
|
|
8
8
|
if (isBrowserScope && typeof PerformanceNavigationTiming !== 'undefined' && !isiOS) {
|
|
9
|
-
onTTFB(({ value,
|
|
10
|
-
if (
|
|
9
|
+
onTTFB(({ value, attribution }) => {
|
|
10
|
+
if (timeToFirstByte.isValid) return
|
|
11
|
+
timeToFirstByte.update({ value, attrs: { navigationEntry: attribution.navigationEntry } })
|
|
11
12
|
})
|
|
12
13
|
} else {
|
|
13
14
|
if (!timeToFirstByte.isValid) {
|
|
@@ -16,6 +17,6 @@ if (isBrowserScope && typeof PerformanceNavigationTiming !== 'undefined' && !isi
|
|
|
16
17
|
for (let key in globalScope?.performance?.timing || {}) entry[key] = Math.max(globalScope?.performance?.timing[key] - offset, 0)
|
|
17
18
|
|
|
18
19
|
// 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,
|
|
20
|
+
timeToFirstByte.update({ value: entry.responseStart, attrs: { navigationEntry: entry } })
|
|
20
21
|
}
|
|
21
22
|
}
|
|
@@ -8,19 +8,18 @@ export class VitalMetric {
|
|
|
8
8
|
this.roundingMethod = typeof roundingMethod === 'function' ? roundingMethod : Math.floor
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
update ({ value,
|
|
11
|
+
update ({ value, attrs = {} }) {
|
|
12
12
|
if (value < 0) return
|
|
13
13
|
const state = {
|
|
14
14
|
value: this.roundingMethod(value),
|
|
15
15
|
name: this.name,
|
|
16
|
-
entries,
|
|
17
16
|
attrs
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
this.history.push(state)
|
|
21
20
|
this.#subscribers.forEach(cb => {
|
|
22
21
|
try {
|
|
23
|
-
cb(
|
|
22
|
+
cb(state)
|
|
24
23
|
} catch (e) {
|
|
25
24
|
// ignore errors
|
|
26
25
|
}
|
|
@@ -31,7 +30,6 @@ export class VitalMetric {
|
|
|
31
30
|
return this.history[this.history.length - 1] || {
|
|
32
31
|
value: undefined,
|
|
33
32
|
name: this.name,
|
|
34
|
-
entries: [],
|
|
35
33
|
attrs: {}
|
|
36
34
|
}
|
|
37
35
|
}
|
|
@@ -33,7 +33,8 @@ export class Aggregate extends AggregateBase {
|
|
|
33
33
|
this.drain()
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
const
|
|
36
|
+
const agentRuntime = getRuntime(agentIdentifier)
|
|
37
|
+
const denyList = agentRuntime.denyList
|
|
37
38
|
setDenyList(denyList)
|
|
38
39
|
|
|
39
40
|
let ajaxEvents = []
|
|
@@ -125,7 +126,7 @@ export class Aggregate extends AggregateBase {
|
|
|
125
126
|
if (xhrContext.dt) {
|
|
126
127
|
event.spanId = xhrContext.dt.spanId
|
|
127
128
|
event.traceId = xhrContext.dt.traceId
|
|
128
|
-
event.spanTimestamp = xhrContext.dt.timestamp
|
|
129
|
+
event.spanTimestamp = agentRuntime.timeKeeper.correctAbsoluteTimestamp(xhrContext.dt.timestamp)
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
// parsed from the AJAX body, looking for operationName param & parsing query for operationType
|
|
@@ -8,7 +8,6 @@ import { id } from '../../../common/ids/id'
|
|
|
8
8
|
import { ffVersion, globalScope, isBrowserScope } from '../../../common/constants/runtime'
|
|
9
9
|
import { dataSize } from '../../../common/util/data-size'
|
|
10
10
|
import { eventListenerOpts } from '../../../common/event-listener/event-listener-opts'
|
|
11
|
-
import { now } from '../../../common/timing/now'
|
|
12
11
|
import { wrapFetch, wrapXhr } from '../../../common/wrap'
|
|
13
12
|
import { parseUrl } from '../../../common/url/parse-url'
|
|
14
13
|
import { DT } from './distributed-tracing'
|
|
@@ -17,6 +16,7 @@ import { InstrumentBase } from '../../utils/instrument-base'
|
|
|
17
16
|
import { FEATURE_NAME } from '../constants'
|
|
18
17
|
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
19
18
|
import { SUPPORTABILITY_METRIC } from '../../metrics/constants'
|
|
19
|
+
import { now } from '../../../common/timing/now'
|
|
20
20
|
|
|
21
21
|
var handlers = ['load', 'error', 'abort', 'timeout']
|
|
22
22
|
var handlersLen = handlers.length
|
|
@@ -14,7 +14,6 @@ import { stringify } from '../../../common/util/stringify'
|
|
|
14
14
|
import { handle } from '../../../common/event-emitter/handle'
|
|
15
15
|
import { mapOwn } from '../../../common/util/map-own'
|
|
16
16
|
import { getInfo, getConfigurationValue, getRuntime } from '../../../common/config/config'
|
|
17
|
-
import { now } from '../../../common/timing/now'
|
|
18
17
|
import { globalScope } from '../../../common/constants/runtime'
|
|
19
18
|
|
|
20
19
|
import { FEATURE_NAME } from '../constants'
|
|
@@ -22,6 +21,7 @@ import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
|
22
21
|
import { AggregateBase } from '../../utils/aggregate-base'
|
|
23
22
|
import { getNREUMInitializedAgent } from '../../../common/window/nreum'
|
|
24
23
|
import { deregisterDrain } from '../../../common/drain/drain'
|
|
24
|
+
import { now } from '../../../common/timing/now'
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* @typedef {import('./compute-stack-trace.js').StackInfo} StackInfo
|
|
@@ -38,10 +38,13 @@ export class Aggregate extends AggregateBase {
|
|
|
38
38
|
this.bufferedErrorsUnderSpa = {}
|
|
39
39
|
this.currentBody = undefined
|
|
40
40
|
this.errorOnPage = false
|
|
41
|
+
this.replayAborted = false
|
|
41
42
|
|
|
42
43
|
// this will need to change to match whatever ee we use in the instrument
|
|
43
44
|
this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved))
|
|
44
45
|
|
|
46
|
+
this.ee.on('REPLAY_ABORTED', () => { this.replayAborted = true })
|
|
47
|
+
|
|
45
48
|
register('err', (...args) => this.storeError(...args), this.featureName, this.ee)
|
|
46
49
|
register('ierr', (...args) => this.storeError(...args), this.featureName, this.ee)
|
|
47
50
|
register('softNavFlush', (interactionId, wasFinished, softNavAttrs) =>
|
|
@@ -78,9 +81,16 @@ export class Aggregate extends AggregateBase {
|
|
|
78
81
|
payload.qs.ri = releaseIds
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
if (body && body.err && body.err.length
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
if (body && body.err && body.err.length) {
|
|
85
|
+
if (this.replayAborted) {
|
|
86
|
+
body.err.forEach((e) => {
|
|
87
|
+
delete e.params?.hasReplay
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
if (!this.errorOnPage) {
|
|
91
|
+
payload.qs.pve = '1'
|
|
92
|
+
this.errorOnPage = true
|
|
93
|
+
}
|
|
84
94
|
}
|
|
85
95
|
return payload
|
|
86
96
|
}
|
|
@@ -133,7 +143,7 @@ export class Aggregate extends AggregateBase {
|
|
|
133
143
|
return canonicalStackString
|
|
134
144
|
}
|
|
135
145
|
|
|
136
|
-
storeError (err, time, internal, customAttributes) {
|
|
146
|
+
storeError (err, time, internal, customAttributes, hasReplay) {
|
|
137
147
|
// are we in an interaction
|
|
138
148
|
time = time || now()
|
|
139
149
|
const agentRuntime = getRuntime(this.agentIdentifier)
|
|
@@ -173,7 +183,7 @@ export class Aggregate extends AggregateBase {
|
|
|
173
183
|
if (!this.stackReported[bucketHash]) {
|
|
174
184
|
this.stackReported[bucketHash] = true
|
|
175
185
|
params.stack_trace = truncateSize(stackInfo.stackString)
|
|
176
|
-
this.observedAt[bucketHash] = agentRuntime.
|
|
186
|
+
this.observedAt[bucketHash] = agentRuntime.timeKeeper.convertRelativeTimestamp(time)
|
|
177
187
|
} else {
|
|
178
188
|
params.browser_stack_hash = stringHashCode(stackInfo.stackString)
|
|
179
189
|
}
|
|
@@ -189,8 +199,9 @@ export class Aggregate extends AggregateBase {
|
|
|
189
199
|
this.pageviewReported[bucketHash] = true
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
if (
|
|
202
|
+
if (hasReplay && !this.replayAborted) params.hasReplay = hasReplay
|
|
193
203
|
params.firstOccurrenceTimestamp = this.observedAt[bucketHash]
|
|
204
|
+
params.timestamp = this.observedAt[bucketHash]
|
|
194
205
|
|
|
195
206
|
var type = internal ? 'ierr' : 'err'
|
|
196
207
|
var newMetrics = { time }
|
|
@@ -198,7 +209,6 @@ export class Aggregate extends AggregateBase {
|
|
|
198
209
|
// Trace sends the error in its payload, and both trace & replay simply listens for any error to occur.
|
|
199
210
|
const jsErrorEvent = [type, bucketHash, params, newMetrics, customAttributes]
|
|
200
211
|
handle('errorAgg', jsErrorEvent, undefined, FEATURE_NAMES.sessionTrace, this.ee)
|
|
201
|
-
handle('errorAgg', jsErrorEvent, undefined, FEATURE_NAMES.sessionReplay, this.ee)
|
|
202
212
|
// still send EE events for other features such as above, but stop this one from aggregating internal data
|
|
203
213
|
if (this.blocked) return
|
|
204
214
|
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { handle } from '../../../common/event-emitter/handle'
|
|
7
|
-
import { now } from '../../../common/timing/now'
|
|
8
7
|
import { InstrumentBase } from '../../utils/instrument-base'
|
|
9
8
|
import { FEATURE_NAME } from '../constants'
|
|
10
9
|
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
@@ -12,11 +11,14 @@ import { globalScope } from '../../../common/constants/runtime'
|
|
|
12
11
|
import { eventListenerOpts } from '../../../common/event-listener/event-listener-opts'
|
|
13
12
|
import { stringify } from '../../../common/util/stringify'
|
|
14
13
|
import { UncaughtError } from './uncaught-error'
|
|
14
|
+
import { now } from '../../../common/timing/now'
|
|
15
|
+
import { SR_EVENT_EMITTER_TYPES } from '../../session_replay/constants'
|
|
15
16
|
|
|
16
17
|
export class Instrument extends InstrumentBase {
|
|
17
18
|
static featureName = FEATURE_NAME
|
|
18
19
|
|
|
19
20
|
#seenErrors = new Set()
|
|
21
|
+
#replayRunning = false
|
|
20
22
|
|
|
21
23
|
constructor (agentIdentifier, aggregator, auto = true) {
|
|
22
24
|
super(agentIdentifier, aggregator, FEATURE_NAME, auto)
|
|
@@ -36,13 +38,16 @@ export class Instrument extends InstrumentBase {
|
|
|
36
38
|
|
|
37
39
|
this.ee.on('internal-error', (error) => {
|
|
38
40
|
if (!this.abortHandler) return
|
|
39
|
-
handle('ierr', [this.#castError(error), now(), true], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
41
|
+
handle('ierr', [this.#castError(error), now(), true, {}, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
this.ee.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, (isRunning) => {
|
|
45
|
+
this.#replayRunning = isRunning
|
|
40
46
|
})
|
|
41
47
|
|
|
42
48
|
globalScope.addEventListener('unhandledrejection', (promiseRejectionEvent) => {
|
|
43
49
|
if (!this.abortHandler) return
|
|
44
|
-
|
|
45
|
-
handle('err', [this.#castPromiseRejectionEvent(promiseRejectionEvent), now(), false, { unhandledPromiseRejection: 1 }], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
50
|
+
handle('err', [this.#castPromiseRejectionEvent(promiseRejectionEvent), now(), false, { unhandledPromiseRejection: 1 }, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
46
51
|
}, eventListenerOpts(false, this.removeOnAbort?.signal))
|
|
47
52
|
|
|
48
53
|
globalScope.addEventListener('error', (errorEvent) => {
|
|
@@ -57,7 +62,7 @@ export class Instrument extends InstrumentBase {
|
|
|
57
62
|
return
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
handle('err', [this.#castErrorEvent(errorEvent), now()], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
65
|
+
handle('err', [this.#castErrorEvent(errorEvent), now(), false, {}, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
61
66
|
}, eventListenerOpts(false, this.removeOnAbort?.signal))
|
|
62
67
|
|
|
63
68
|
this.abortHandler = this.#abort // we also use this as a flag to denote that the feature is active or on and handling errors
|
|
@@ -86,14 +86,15 @@ export class Aggregate extends AggregateBase {
|
|
|
86
86
|
height = window.document.documentElement.clientHeight
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
const agentRuntime = getRuntime(this.agentIdentifier)
|
|
89
90
|
var defaults = {
|
|
90
|
-
timestamp: t
|
|
91
|
+
timestamp: agentRuntime.timeKeeper.convertRelativeTimestamp(t),
|
|
91
92
|
timeSinceLoad: t / 1000,
|
|
92
93
|
browserWidth: width,
|
|
93
94
|
browserHeight: height,
|
|
94
95
|
referrerUrl: this.referrerUrl,
|
|
95
96
|
currentUrl: cleanURL('' + location),
|
|
96
|
-
pageUrl: cleanURL(
|
|
97
|
+
pageUrl: cleanURL(agentRuntime.origin),
|
|
97
98
|
eventType: 'PageAction'
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -15,6 +15,8 @@ import { drain } from '../../../common/drain/drain'
|
|
|
15
15
|
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
16
16
|
import { handle } from '../../../common/event-emitter/handle'
|
|
17
17
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
|
|
18
|
+
import { now } from '../../../common/timing/now'
|
|
19
|
+
import { TimeKeeper } from '../../../common/timing/time-keeper'
|
|
18
20
|
|
|
19
21
|
export class Aggregate extends AggregateBase {
|
|
20
22
|
static featureName = CONSTANTS.FEATURE_NAME
|
|
@@ -26,8 +28,8 @@ export class Aggregate extends AggregateBase {
|
|
|
26
28
|
this.firstByteToDomContent = 0 // our "dom processing" duration
|
|
27
29
|
|
|
28
30
|
if (isBrowserScope) {
|
|
29
|
-
timeToFirstByte.subscribe(({ value,
|
|
30
|
-
const navEntry =
|
|
31
|
+
timeToFirstByte.subscribe(({ value, attrs }) => {
|
|
32
|
+
const navEntry = attrs.navigationEntry
|
|
31
33
|
this.timeToFirstByte = Math.max(value, this.timeToFirstByte)
|
|
32
34
|
this.firstByteToWindowLoad = Math.max(Math.round(navEntry.loadEventEnd - this.timeToFirstByte), this.firstByteToWindowLoad) // our "frontend" duration
|
|
33
35
|
this.firstByteToDomContent = Math.max(Math.round(navEntry.domContentLoadedEventEnd - this.timeToFirstByte), this.firstByteToDomContent) // our "dom processing" duration
|
|
@@ -100,13 +102,13 @@ export class Aggregate extends AggregateBase {
|
|
|
100
102
|
queryParameters.fp = firstPaint.current.value
|
|
101
103
|
queryParameters.fcp = firstContentfulPaint.current.value
|
|
102
104
|
|
|
103
|
-
const rumStartTime =
|
|
105
|
+
const rumStartTime = now()
|
|
104
106
|
harvester.send({
|
|
105
107
|
endpoint: 'rum',
|
|
106
108
|
payload: { qs: queryParameters, body },
|
|
107
109
|
opts: { needResponse: true, sendEmptyBody: true },
|
|
108
110
|
cbFinished: ({ status, responseText, xhr }) => {
|
|
109
|
-
const rumEndTime =
|
|
111
|
+
const rumEndTime = now()
|
|
110
112
|
|
|
111
113
|
if (status >= 400 || status === 0) {
|
|
112
114
|
// Adding retry logic for the rum call will be a separate change
|
|
@@ -115,7 +117,11 @@ export class Aggregate extends AggregateBase {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
try {
|
|
118
|
-
|
|
120
|
+
const timeKeeper = new TimeKeeper()
|
|
121
|
+
timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime)
|
|
122
|
+
if (!timeKeeper.ready) throw new Error('TimeKeeper not ready')
|
|
123
|
+
|
|
124
|
+
agentRuntime.timeKeeper = timeKeeper
|
|
119
125
|
} catch (error) {
|
|
120
126
|
handle(SUPPORTABILITY_METRIC_CHANNEL, ['PVE/NRTime/Calculation/Failed'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
121
127
|
drain(this.agentIdentifier, FEATURE_NAMES.metrics, true)
|
|
@@ -20,6 +20,8 @@ import { interactionToNextPaint } from '../../../common/vitals/interaction-to-ne
|
|
|
20
20
|
import { largestContentfulPaint } from '../../../common/vitals/largest-contentful-paint'
|
|
21
21
|
import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
|
|
22
22
|
import { longTask } from '../../../common/vitals/long-task'
|
|
23
|
+
import { subscribeToVisibilityChange } from '../../../common/window/page-visibility'
|
|
24
|
+
import { VITAL_NAMES } from '../../../common/vitals/constants'
|
|
23
25
|
|
|
24
26
|
export class Aggregate extends AggregateBase {
|
|
25
27
|
static featureName = FEATURE_NAME
|
|
@@ -37,9 +39,6 @@ export class Aggregate extends AggregateBase {
|
|
|
37
39
|
|
|
38
40
|
if (getConfigurationValue(this.agentIdentifier, 'page_view_timing.long_task') === true) longTask.subscribe(this.#handleVitalMetric)
|
|
39
41
|
|
|
40
|
-
/* It's important that CWV api, like "onLCP", is called before this scheduler is initialized. The reason is because they listen to the same
|
|
41
|
-
on vis change or pagehide events, and we'd want ex. onLCP to record the timing (win the race) before we try to send "final harvest". */
|
|
42
|
-
|
|
43
42
|
registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee)
|
|
44
43
|
registerHandler('winPagehide', msTimestamp => this.recordPageUnload(msTimestamp), this.featureName, this.ee)
|
|
45
44
|
|
|
@@ -47,14 +46,24 @@ export class Aggregate extends AggregateBase {
|
|
|
47
46
|
const harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.harvestTimeSeconds') || 30
|
|
48
47
|
|
|
49
48
|
this.waitForFlags(([])).then(() => {
|
|
49
|
+
/* It's important that CWV api, like "onLCP", is called before the **scheduler** is initialized. The reason is because they listen to the same
|
|
50
|
+
on vis change or pagehide events, and we'd want ex. onLCP to record the timing (win the race) before we try to send "final harvest". */
|
|
50
51
|
firstPaint.subscribe(this.#handleVitalMetric)
|
|
51
52
|
firstContentfulPaint.subscribe(this.#handleVitalMetric)
|
|
52
53
|
firstInputDelay.subscribe(this.#handleVitalMetric)
|
|
53
54
|
largestContentfulPaint.subscribe(this.#handleVitalMetric)
|
|
54
55
|
interactionToNextPaint.subscribe(this.#handleVitalMetric)
|
|
55
|
-
timeToFirstByte.subscribe(({
|
|
56
|
-
this.addTiming('load', Math.round(
|
|
56
|
+
timeToFirstByte.subscribe(({ attrs }) => {
|
|
57
|
+
this.addTiming('load', Math.round(attrs.navigationEntry.loadEventEnd))
|
|
57
58
|
})
|
|
59
|
+
subscribeToVisibilityChange(() => {
|
|
60
|
+
/* Downstream, the event consumer interprets all timing node value as ms-unit and converts it to seconds via division by 1000. CLS is unitless so this normally is a problem.
|
|
61
|
+
bel.6 schema also doesn't support decimal values, of which cls within [0,1). However, the two nicely cancels out, and we can multiply cls by 1000 to both negate the division
|
|
62
|
+
and send an integer > 1. We effectively lose some precision down to 3 decimal places for this workaround. E.g. (real) 0.749132... -> 749.132...-> 749 -> 0.749 (final) */
|
|
63
|
+
const { name, value, attrs } = cumulativeLayoutShift.current
|
|
64
|
+
if (value === undefined) return
|
|
65
|
+
this.addTiming(name, value * 1000, attrs)
|
|
66
|
+
}, true) // CLS node should only reports on vis change rather than on every change
|
|
58
67
|
|
|
59
68
|
const scheduler = new HarvestScheduler('events', {
|
|
60
69
|
onFinished: (...args) => this.onHarvestFinished(...args),
|
|
@@ -102,8 +111,9 @@ export class Aggregate extends AggregateBase {
|
|
|
102
111
|
Issue: Because NR 'pageHide' was only sent once with what is considered the "final" CLS value, in the case that 'pageHide' fires before 'load' happens, we incorrectly a final CLS of 0 for that page.
|
|
103
112
|
Mitigation: We've set initial CLS to null so that it's omitted from timings like 'pageHide' in that edge case. It should only be included if onCLS callback was executed at least once.
|
|
104
113
|
Future: onCLS value changes should be reported directly & CLS separated into its own timing node so it's not beholden to 'pageHide' firing. It'd also be possible to report the real final CLS.
|
|
114
|
+
*cli Mar'24 update: CLS now emitted as its own timing node in addition to as-property under other nodes. The 'cls' property is unnecessary for cls nodes.
|
|
105
115
|
*/
|
|
106
|
-
if (cumulativeLayoutShift.current.value >= 0) {
|
|
116
|
+
if (name !== VITAL_NAMES.CUMULATIVE_LAYOUT_SHIFT && cumulativeLayoutShift.current.value >= 0) {
|
|
107
117
|
attrs.cls = cumulativeLayoutShift.current.value
|
|
108
118
|
}
|
|
109
119
|
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
import { handle } from '../../../common/event-emitter/handle'
|
|
6
6
|
import { subscribeToVisibilityChange } from '../../../common/window/page-visibility'
|
|
7
7
|
import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
|
|
8
|
-
import { now } from '../../../common/timing/now'
|
|
9
8
|
import { InstrumentBase } from '../../utils/instrument-base'
|
|
10
9
|
import { FEATURE_NAME } from '../constants'
|
|
11
10
|
import { isBrowserScope } from '../../../common/constants/runtime'
|
|
11
|
+
import { now } from '../../../common/timing/now'
|
|
12
12
|
|
|
13
13
|
export class Instrument extends InstrumentBase {
|
|
14
14
|
static featureName = FEATURE_NAME
|