@datadog/browser-rum-core
Advanced tools
Comparing version 2.7.2 to 2.7.4
@@ -7,4 +7,4 @@ "use strict"; | ||
datacenter: 'us', | ||
sdkVersion: '2.7.2', | ||
sdkVersion: '2.7.4', | ||
}; | ||
//# sourceMappingURL=buildEnv.js.map |
@@ -7,3 +7,3 @@ import { Context, RelativeTime } from '@datadog/browser-core'; | ||
import { AutoAction, AutoActionCreatedEvent } from './rumEventsCollection/action/trackActions'; | ||
import { View, ViewCreatedEvent } from './rumEventsCollection/view/trackViews'; | ||
import { ViewEvent, ViewCreatedEvent } from './rumEventsCollection/view/trackViews'; | ||
export declare enum LifeCycleEventType { | ||
@@ -38,3 +38,3 @@ PERFORMANCE_ENTRY_COLLECTED = 0, | ||
notify(eventType: LifeCycleEventType.VIEW_CREATED, data: ViewCreatedEvent): void; | ||
notify(eventType: LifeCycleEventType.VIEW_UPDATED, data: View): void; | ||
notify(eventType: LifeCycleEventType.VIEW_UPDATED, data: ViewEvent): void; | ||
notify(eventType: LifeCycleEventType.SESSION_RENEWED | LifeCycleEventType.DOM_MUTATED | LifeCycleEventType.BEFORE_UNLOAD | LifeCycleEventType.AUTO_ACTION_DISCARDED | LifeCycleEventType.VIEW_ENDED | LifeCycleEventType.RECORD_STARTED | LifeCycleEventType.RECORD_STOPPED): void; | ||
@@ -54,3 +54,3 @@ notify(eventType: LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, data: { | ||
subscribe(eventType: LifeCycleEventType.VIEW_CREATED, callback: (data: ViewCreatedEvent) => void): Subscription; | ||
subscribe(eventType: LifeCycleEventType.VIEW_UPDATED, callback: (data: View) => void): Subscription; | ||
subscribe(eventType: LifeCycleEventType.VIEW_UPDATED, callback: (data: ViewEvent) => void): Subscription; | ||
subscribe(eventType: LifeCycleEventType.SESSION_RENEWED | LifeCycleEventType.DOM_MUTATED | LifeCycleEventType.BEFORE_UNLOAD | LifeCycleEventType.AUTO_ACTION_DISCARDED | LifeCycleEventType.VIEW_ENDED | LifeCycleEventType.RECORD_STARTED | LifeCycleEventType.RECORD_STOPPED, callback: () => void): Subscription; | ||
@@ -57,0 +57,0 @@ subscribe(eventType: LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, callback: (data: { |
@@ -5,4 +5,4 @@ import { Duration, RelativeTime } from '@datadog/browser-core'; | ||
import { ViewLoadingType, ViewCustomTimings } from '../../../rawRumEvent.types'; | ||
import { Timings } from './trackTimings'; | ||
export interface View { | ||
import { Timings } from './trackInitialViewTimings'; | ||
export interface ViewEvent { | ||
id: string; | ||
@@ -9,0 +9,0 @@ name?: string; |
@@ -6,27 +6,19 @@ "use strict"; | ||
var browser_core_1 = require("@datadog/browser-core"); | ||
var performanceCollection_1 = require("../../../browser/performanceCollection"); | ||
var lifeCycle_1 = require("../../lifeCycle"); | ||
var trackEventCounts_1 = require("../../trackEventCounts"); | ||
var trackPageActivities_1 = require("../../trackPageActivities"); | ||
var rawRumEvent_types_1 = require("../../../rawRumEvent.types"); | ||
var trackTimings_1 = require("./trackTimings"); | ||
var trackInitialViewTimings_1 = require("./trackInitialViewTimings"); | ||
var trackViewMetrics_1 = require("./trackViewMetrics"); | ||
var trackLocationChanges_1 = require("./trackLocationChanges"); | ||
exports.THROTTLE_VIEW_UPDATE_PERIOD = 3000; | ||
exports.SESSION_KEEP_ALIVE_INTERVAL = 5 * browser_core_1.ONE_MINUTE; | ||
function trackViews(location, lifeCycle) { | ||
var startOrigin = 0; | ||
var hasReplay = false; | ||
var initialView = newView(lifeCycle, location, hasReplay, rawRumEvent_types_1.ViewLoadingType.INITIAL_LOAD, document.referrer, startOrigin); | ||
var currentView = initialView; | ||
var stopTimingsTracking = trackTimings_1.trackTimings(lifeCycle, function (timings) { | ||
initialView.updateTimings(timings); | ||
initialView.scheduleUpdate(); | ||
}).stop; | ||
var stopHistoryTracking = trackHistory(onLocationChange).stop; | ||
var stopHashTracking = trackHash(onLocationChange).stop; | ||
function onLocationChange() { | ||
if (currentView.isDifferentView(location)) { | ||
var isRecording = false; | ||
// eslint-disable-next-line prefer-const | ||
var _a = trackInitialView(), stopInitialViewTracking = _a.stop, currentView = _a.initialView; | ||
var stopLocationChangesTracking = trackLocationChanges_1.trackLocationChanges(function () { | ||
if (trackLocationChanges_1.areDifferentLocation(currentView.getLocation(), location)) { | ||
// Renew view on location changes | ||
currentView.end(); | ||
currentView.triggerUpdate(); | ||
currentView = newView(lifeCycle, location, hasReplay, rawRumEvent_types_1.ViewLoadingType.ROUTE_CHANGE, currentView.url); | ||
currentView = trackViewChange(); | ||
return; | ||
@@ -36,3 +28,3 @@ } | ||
currentView.triggerUpdate(); | ||
} | ||
}).stop; | ||
// Renew view on session renewal | ||
@@ -42,3 +34,3 @@ lifeCycle.subscribe(lifeCycle_1.LifeCycleEventType.SESSION_RENEWED, function () { | ||
currentView.end(); | ||
currentView = newView(lifeCycle, location, hasReplay, rawRumEvent_types_1.ViewLoadingType.ROUTE_CHANGE, currentView.url); | ||
currentView = trackViewChange(); | ||
}); | ||
@@ -51,7 +43,7 @@ // End the current view on page unload | ||
lifeCycle.subscribe(lifeCycle_1.LifeCycleEventType.RECORD_STARTED, function () { | ||
hasReplay = true; | ||
isRecording = true; | ||
currentView.updateHasReplay(true); | ||
}); | ||
lifeCycle.subscribe(lifeCycle_1.LifeCycleEventType.RECORD_STOPPED, function () { | ||
hasReplay = false; | ||
isRecording = false; | ||
}); | ||
@@ -62,2 +54,14 @@ // Session keep alive | ||
}), exports.SESSION_KEEP_ALIVE_INTERVAL); | ||
function trackInitialView() { | ||
var startOrigin = 0; | ||
var initialView = newView(lifeCycle, location, isRecording, rawRumEvent_types_1.ViewLoadingType.INITIAL_LOAD, document.referrer, startOrigin); | ||
var stop = trackInitialViewTimings_1.trackInitialViewTimings(lifeCycle, function (timings) { | ||
initialView.updateTimings(timings); | ||
initialView.scheduleUpdate(); | ||
}).stop; | ||
return { initialView: initialView, stop: stop }; | ||
} | ||
function trackViewChange() { | ||
return newView(lifeCycle, location, isRecording, rawRumEvent_types_1.ViewLoadingType.ROUTE_CHANGE, currentView.url); | ||
} | ||
return { | ||
@@ -70,5 +74,4 @@ addTiming: function (name, time) { | ||
stop: function () { | ||
stopHistoryTracking(); | ||
stopHashTracking(); | ||
stopTimingsTracking(); | ||
stopInitialViewTracking(); | ||
stopLocationChangesTracking(); | ||
currentView.end(); | ||
@@ -84,13 +87,5 @@ clearInterval(keepAliveInterval); | ||
var id = browser_core_1.generateUUID(); | ||
var eventCounts = { | ||
errorCount: 0, | ||
longTaskCount: 0, | ||
resourceCount: 0, | ||
userActionCount: 0, | ||
}; | ||
var timings = {}; | ||
var customTimings = {}; | ||
var documentVersion = 0; | ||
var cumulativeLayoutShift; | ||
var loadingTime; | ||
var endTime; | ||
@@ -104,22 +99,3 @@ var location = tslib_1.__assign({}, initialLocation); | ||
}), scheduleViewUpdate = _a.throttled, cancelScheduleViewUpdate = _a.cancel; | ||
var stopEventCountsTracking = trackEventCounts_1.trackEventCounts(lifeCycle, function (newEventCounts) { | ||
eventCounts = newEventCounts; | ||
scheduleViewUpdate(); | ||
}).stop; | ||
var _b = trackLoadingTime(loadingType, function (newLoadingTime) { | ||
loadingTime = newLoadingTime; | ||
scheduleViewUpdate(); | ||
}), setActivityLoadingTime = _b.setActivityLoadingTime, setLoadEvent = _b.setLoadEvent; | ||
var stopActivityLoadingTimeTracking = trackActivityLoadingTime(lifeCycle, setActivityLoadingTime).stop; | ||
var stopCLSTracking; | ||
if (isLayoutShiftSupported()) { | ||
cumulativeLayoutShift = 0; | ||
(stopCLSTracking = trackLayoutShift(lifeCycle, function (layoutShift) { | ||
cumulativeLayoutShift += layoutShift; | ||
scheduleViewUpdate(); | ||
}).stop); | ||
} | ||
else { | ||
stopCLSTracking = browser_core_1.noop; | ||
} | ||
var _b = trackViewMetrics_1.trackViewMetrics(lifeCycle, scheduleViewUpdate, loadingType), setLoadEvent = _b.setLoadEvent, stopViewMetricsTracking = _b.stop, viewMetrics = _b.viewMetrics; | ||
// Initial view update | ||
@@ -129,10 +105,6 @@ triggerViewUpdate(); | ||
documentVersion += 1; | ||
lifeCycle.notify(lifeCycle_1.LifeCycleEventType.VIEW_UPDATED, { | ||
cumulativeLayoutShift: cumulativeLayoutShift && browser_core_1.round(cumulativeLayoutShift, 4), | ||
customTimings: customTimings, | ||
lifeCycle.notify(lifeCycle_1.LifeCycleEventType.VIEW_UPDATED, tslib_1.__assign(tslib_1.__assign({}, viewMetrics), { customTimings: customTimings, | ||
documentVersion: documentVersion, | ||
eventCounts: eventCounts, | ||
id: id, | ||
name: name, | ||
loadingTime: loadingTime, | ||
loadingType: loadingType, | ||
@@ -143,6 +115,3 @@ location: location, | ||
startTime: startTime, | ||
timings: timings, | ||
duration: browser_core_1.elapsed(startTime, endTime === undefined ? browser_core_1.relativeNow() : endTime), | ||
isActive: endTime === undefined, | ||
}); | ||
timings: timings, duration: browser_core_1.elapsed(startTime, endTime === undefined ? browser_core_1.relativeNow() : endTime), isActive: endTime === undefined })); | ||
} | ||
@@ -153,11 +122,5 @@ return { | ||
endTime = browser_core_1.relativeNow(); | ||
stopEventCountsTracking(); | ||
stopActivityLoadingTimeTracking(); | ||
stopCLSTracking(); | ||
stopViewMetricsTracking(); | ||
lifeCycle.notify(lifeCycle_1.LifeCycleEventType.VIEW_ENDED); | ||
}, | ||
isDifferentView: function (otherLocation) { | ||
return (location.pathname !== otherLocation.pathname || | ||
(!isHashAnAnchor(otherLocation.hash) && otherLocation.hash !== location.hash)); | ||
}, | ||
getLocation: function () { | ||
@@ -191,96 +154,3 @@ return location; | ||
} | ||
function isHashAnAnchor(hash) { | ||
var correspondingId = hash.substr(1); | ||
return !!document.getElementById(correspondingId); | ||
} | ||
function trackHistory(onHistoryChange) { | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
var originalPushState = history.pushState; | ||
history.pushState = browser_core_1.monitor(function () { | ||
originalPushState.apply(this, arguments); | ||
onHistoryChange(); | ||
}); | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
var originalReplaceState = history.replaceState; | ||
history.replaceState = browser_core_1.monitor(function () { | ||
originalReplaceState.apply(this, arguments); | ||
onHistoryChange(); | ||
}); | ||
var removeListener = browser_core_1.addEventListener(window, "popstate" /* POP_STATE */, onHistoryChange).stop; | ||
var stop = function () { | ||
removeListener(); | ||
history.pushState = originalPushState; | ||
history.replaceState = originalReplaceState; | ||
}; | ||
return { stop: stop }; | ||
} | ||
function trackHash(onHashChange) { | ||
return browser_core_1.addEventListener(window, "hashchange" /* HASH_CHANGE */, onHashChange); | ||
} | ||
function trackLoadingTime(loadType, callback) { | ||
var isWaitingForLoadEvent = loadType === rawRumEvent_types_1.ViewLoadingType.INITIAL_LOAD; | ||
var isWaitingForActivityLoadingTime = true; | ||
var loadingTimeCandidates = []; | ||
function invokeCallbackIfAllCandidatesAreReceived() { | ||
if (!isWaitingForActivityLoadingTime && !isWaitingForLoadEvent && loadingTimeCandidates.length > 0) { | ||
callback(Math.max.apply(Math, loadingTimeCandidates)); | ||
} | ||
} | ||
return { | ||
setLoadEvent: function (loadEvent) { | ||
if (isWaitingForLoadEvent) { | ||
isWaitingForLoadEvent = false; | ||
loadingTimeCandidates.push(loadEvent); | ||
invokeCallbackIfAllCandidatesAreReceived(); | ||
} | ||
}, | ||
setActivityLoadingTime: function (activityLoadingTime) { | ||
if (isWaitingForActivityLoadingTime) { | ||
isWaitingForActivityLoadingTime = false; | ||
if (activityLoadingTime !== undefined) { | ||
loadingTimeCandidates.push(activityLoadingTime); | ||
} | ||
invokeCallbackIfAllCandidatesAreReceived(); | ||
} | ||
}, | ||
}; | ||
} | ||
function trackActivityLoadingTime(lifeCycle, callback) { | ||
var startTime = browser_core_1.relativeNow(); | ||
var stopWaitIdlePageActivity = trackPageActivities_1.waitIdlePageActivity(lifeCycle, function (hadActivity, endTime) { | ||
if (hadActivity) { | ||
callback(browser_core_1.elapsed(startTime, endTime)); | ||
} | ||
else { | ||
callback(undefined); | ||
} | ||
}).stop; | ||
return { stop: stopWaitIdlePageActivity }; | ||
} | ||
/** | ||
* Track layout shifts (LS) occurring during the Views. This yields multiple values that can be | ||
* added up to compute the cumulated layout shift (CLS). | ||
* | ||
* See isLayoutShiftSupported to check for browser support. | ||
* | ||
* Documentation: https://web.dev/cls/ | ||
* Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/master/src/getCLS.ts | ||
*/ | ||
function trackLayoutShift(lifeCycle, callback) { | ||
var stop = lifeCycle.subscribe(lifeCycle_1.LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, function (entry) { | ||
if (entry.entryType === 'layout-shift' && !entry.hadRecentInput) { | ||
callback(entry.value); | ||
} | ||
}).unsubscribe; | ||
return { | ||
stop: stop, | ||
}; | ||
} | ||
/** | ||
* Check whether `layout-shift` is supported by the browser. | ||
*/ | ||
function isLayoutShiftSupported() { | ||
return performanceCollection_1.supportPerformanceTimingEvent('layout-shift'); | ||
} | ||
/** | ||
* Timing name is used as facet path that must contain only letters, digits, or the characters - _ . @ $ | ||
@@ -287,0 +157,0 @@ */ |
export var buildEnv = { | ||
buildMode: 'release', | ||
datacenter: 'us', | ||
sdkVersion: '2.7.2', | ||
sdkVersion: '2.7.4', | ||
}; | ||
//# sourceMappingURL=buildEnv.js.map |
@@ -7,3 +7,3 @@ import { Context, RelativeTime } from '@datadog/browser-core'; | ||
import { AutoAction, AutoActionCreatedEvent } from './rumEventsCollection/action/trackActions'; | ||
import { View, ViewCreatedEvent } from './rumEventsCollection/view/trackViews'; | ||
import { ViewEvent, ViewCreatedEvent } from './rumEventsCollection/view/trackViews'; | ||
export declare enum LifeCycleEventType { | ||
@@ -38,3 +38,3 @@ PERFORMANCE_ENTRY_COLLECTED = 0, | ||
notify(eventType: LifeCycleEventType.VIEW_CREATED, data: ViewCreatedEvent): void; | ||
notify(eventType: LifeCycleEventType.VIEW_UPDATED, data: View): void; | ||
notify(eventType: LifeCycleEventType.VIEW_UPDATED, data: ViewEvent): void; | ||
notify(eventType: LifeCycleEventType.SESSION_RENEWED | LifeCycleEventType.DOM_MUTATED | LifeCycleEventType.BEFORE_UNLOAD | LifeCycleEventType.AUTO_ACTION_DISCARDED | LifeCycleEventType.VIEW_ENDED | LifeCycleEventType.RECORD_STARTED | LifeCycleEventType.RECORD_STOPPED): void; | ||
@@ -54,3 +54,3 @@ notify(eventType: LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, data: { | ||
subscribe(eventType: LifeCycleEventType.VIEW_CREATED, callback: (data: ViewCreatedEvent) => void): Subscription; | ||
subscribe(eventType: LifeCycleEventType.VIEW_UPDATED, callback: (data: View) => void): Subscription; | ||
subscribe(eventType: LifeCycleEventType.VIEW_UPDATED, callback: (data: ViewEvent) => void): Subscription; | ||
subscribe(eventType: LifeCycleEventType.SESSION_RENEWED | LifeCycleEventType.DOM_MUTATED | LifeCycleEventType.BEFORE_UNLOAD | LifeCycleEventType.AUTO_ACTION_DISCARDED | LifeCycleEventType.VIEW_ENDED | LifeCycleEventType.RECORD_STARTED | LifeCycleEventType.RECORD_STOPPED, callback: () => void): Subscription; | ||
@@ -57,0 +57,0 @@ subscribe(eventType: LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, callback: (data: { |
@@ -5,4 +5,4 @@ import { Duration, RelativeTime } from '@datadog/browser-core'; | ||
import { ViewLoadingType, ViewCustomTimings } from '../../../rawRumEvent.types'; | ||
import { Timings } from './trackTimings'; | ||
export interface View { | ||
import { Timings } from './trackInitialViewTimings'; | ||
export interface ViewEvent { | ||
id: string; | ||
@@ -9,0 +9,0 @@ name?: string; |
import { __assign } from "tslib"; | ||
import { addEventListener, elapsed, generateUUID, monitor, noop, ONE_MINUTE, relativeNow, round, throttle, } from '@datadog/browser-core'; | ||
import { supportPerformanceTimingEvent } from '../../../browser/performanceCollection'; | ||
import { elapsed, generateUUID, monitor, ONE_MINUTE, relativeNow, throttle, } from '@datadog/browser-core'; | ||
import { LifeCycleEventType } from '../../lifeCycle'; | ||
import { trackEventCounts } from '../../trackEventCounts'; | ||
import { waitIdlePageActivity } from '../../trackPageActivities'; | ||
import { ViewLoadingType } from '../../../rawRumEvent.types'; | ||
import { trackTimings } from './trackTimings'; | ||
import { trackInitialViewTimings } from './trackInitialViewTimings'; | ||
import { trackViewMetrics } from './trackViewMetrics'; | ||
import { trackLocationChanges, areDifferentLocation } from './trackLocationChanges'; | ||
export var THROTTLE_VIEW_UPDATE_PERIOD = 3000; | ||
export var SESSION_KEEP_ALIVE_INTERVAL = 5 * ONE_MINUTE; | ||
export function trackViews(location, lifeCycle) { | ||
var startOrigin = 0; | ||
var hasReplay = false; | ||
var initialView = newView(lifeCycle, location, hasReplay, ViewLoadingType.INITIAL_LOAD, document.referrer, startOrigin); | ||
var currentView = initialView; | ||
var stopTimingsTracking = trackTimings(lifeCycle, function (timings) { | ||
initialView.updateTimings(timings); | ||
initialView.scheduleUpdate(); | ||
}).stop; | ||
var stopHistoryTracking = trackHistory(onLocationChange).stop; | ||
var stopHashTracking = trackHash(onLocationChange).stop; | ||
function onLocationChange() { | ||
if (currentView.isDifferentView(location)) { | ||
var isRecording = false; | ||
// eslint-disable-next-line prefer-const | ||
var _a = trackInitialView(), stopInitialViewTracking = _a.stop, currentView = _a.initialView; | ||
var stopLocationChangesTracking = trackLocationChanges(function () { | ||
if (areDifferentLocation(currentView.getLocation(), location)) { | ||
// Renew view on location changes | ||
currentView.end(); | ||
currentView.triggerUpdate(); | ||
currentView = newView(lifeCycle, location, hasReplay, ViewLoadingType.ROUTE_CHANGE, currentView.url); | ||
currentView = trackViewChange(); | ||
return; | ||
@@ -32,3 +24,3 @@ } | ||
currentView.triggerUpdate(); | ||
} | ||
}).stop; | ||
// Renew view on session renewal | ||
@@ -38,3 +30,3 @@ lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, function () { | ||
currentView.end(); | ||
currentView = newView(lifeCycle, location, hasReplay, ViewLoadingType.ROUTE_CHANGE, currentView.url); | ||
currentView = trackViewChange(); | ||
}); | ||
@@ -47,7 +39,7 @@ // End the current view on page unload | ||
lifeCycle.subscribe(LifeCycleEventType.RECORD_STARTED, function () { | ||
hasReplay = true; | ||
isRecording = true; | ||
currentView.updateHasReplay(true); | ||
}); | ||
lifeCycle.subscribe(LifeCycleEventType.RECORD_STOPPED, function () { | ||
hasReplay = false; | ||
isRecording = false; | ||
}); | ||
@@ -58,2 +50,14 @@ // Session keep alive | ||
}), SESSION_KEEP_ALIVE_INTERVAL); | ||
function trackInitialView() { | ||
var startOrigin = 0; | ||
var initialView = newView(lifeCycle, location, isRecording, ViewLoadingType.INITIAL_LOAD, document.referrer, startOrigin); | ||
var stop = trackInitialViewTimings(lifeCycle, function (timings) { | ||
initialView.updateTimings(timings); | ||
initialView.scheduleUpdate(); | ||
}).stop; | ||
return { initialView: initialView, stop: stop }; | ||
} | ||
function trackViewChange() { | ||
return newView(lifeCycle, location, isRecording, ViewLoadingType.ROUTE_CHANGE, currentView.url); | ||
} | ||
return { | ||
@@ -66,5 +70,4 @@ addTiming: function (name, time) { | ||
stop: function () { | ||
stopHistoryTracking(); | ||
stopHashTracking(); | ||
stopTimingsTracking(); | ||
stopInitialViewTracking(); | ||
stopLocationChangesTracking(); | ||
currentView.end(); | ||
@@ -79,13 +82,5 @@ clearInterval(keepAliveInterval); | ||
var id = generateUUID(); | ||
var eventCounts = { | ||
errorCount: 0, | ||
longTaskCount: 0, | ||
resourceCount: 0, | ||
userActionCount: 0, | ||
}; | ||
var timings = {}; | ||
var customTimings = {}; | ||
var documentVersion = 0; | ||
var cumulativeLayoutShift; | ||
var loadingTime; | ||
var endTime; | ||
@@ -99,22 +94,3 @@ var location = __assign({}, initialLocation); | ||
}), scheduleViewUpdate = _a.throttled, cancelScheduleViewUpdate = _a.cancel; | ||
var stopEventCountsTracking = trackEventCounts(lifeCycle, function (newEventCounts) { | ||
eventCounts = newEventCounts; | ||
scheduleViewUpdate(); | ||
}).stop; | ||
var _b = trackLoadingTime(loadingType, function (newLoadingTime) { | ||
loadingTime = newLoadingTime; | ||
scheduleViewUpdate(); | ||
}), setActivityLoadingTime = _b.setActivityLoadingTime, setLoadEvent = _b.setLoadEvent; | ||
var stopActivityLoadingTimeTracking = trackActivityLoadingTime(lifeCycle, setActivityLoadingTime).stop; | ||
var stopCLSTracking; | ||
if (isLayoutShiftSupported()) { | ||
cumulativeLayoutShift = 0; | ||
(stopCLSTracking = trackLayoutShift(lifeCycle, function (layoutShift) { | ||
cumulativeLayoutShift += layoutShift; | ||
scheduleViewUpdate(); | ||
}).stop); | ||
} | ||
else { | ||
stopCLSTracking = noop; | ||
} | ||
var _b = trackViewMetrics(lifeCycle, scheduleViewUpdate, loadingType), setLoadEvent = _b.setLoadEvent, stopViewMetricsTracking = _b.stop, viewMetrics = _b.viewMetrics; | ||
// Initial view update | ||
@@ -124,10 +100,6 @@ triggerViewUpdate(); | ||
documentVersion += 1; | ||
lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, { | ||
cumulativeLayoutShift: cumulativeLayoutShift && round(cumulativeLayoutShift, 4), | ||
customTimings: customTimings, | ||
lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, __assign(__assign({}, viewMetrics), { customTimings: customTimings, | ||
documentVersion: documentVersion, | ||
eventCounts: eventCounts, | ||
id: id, | ||
name: name, | ||
loadingTime: loadingTime, | ||
loadingType: loadingType, | ||
@@ -138,6 +110,3 @@ location: location, | ||
startTime: startTime, | ||
timings: timings, | ||
duration: elapsed(startTime, endTime === undefined ? relativeNow() : endTime), | ||
isActive: endTime === undefined, | ||
}); | ||
timings: timings, duration: elapsed(startTime, endTime === undefined ? relativeNow() : endTime), isActive: endTime === undefined })); | ||
} | ||
@@ -148,11 +117,5 @@ return { | ||
endTime = relativeNow(); | ||
stopEventCountsTracking(); | ||
stopActivityLoadingTimeTracking(); | ||
stopCLSTracking(); | ||
stopViewMetricsTracking(); | ||
lifeCycle.notify(LifeCycleEventType.VIEW_ENDED); | ||
}, | ||
isDifferentView: function (otherLocation) { | ||
return (location.pathname !== otherLocation.pathname || | ||
(!isHashAnAnchor(otherLocation.hash) && otherLocation.hash !== location.hash)); | ||
}, | ||
getLocation: function () { | ||
@@ -186,96 +149,3 @@ return location; | ||
} | ||
function isHashAnAnchor(hash) { | ||
var correspondingId = hash.substr(1); | ||
return !!document.getElementById(correspondingId); | ||
} | ||
function trackHistory(onHistoryChange) { | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
var originalPushState = history.pushState; | ||
history.pushState = monitor(function () { | ||
originalPushState.apply(this, arguments); | ||
onHistoryChange(); | ||
}); | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
var originalReplaceState = history.replaceState; | ||
history.replaceState = monitor(function () { | ||
originalReplaceState.apply(this, arguments); | ||
onHistoryChange(); | ||
}); | ||
var removeListener = addEventListener(window, "popstate" /* POP_STATE */, onHistoryChange).stop; | ||
var stop = function () { | ||
removeListener(); | ||
history.pushState = originalPushState; | ||
history.replaceState = originalReplaceState; | ||
}; | ||
return { stop: stop }; | ||
} | ||
function trackHash(onHashChange) { | ||
return addEventListener(window, "hashchange" /* HASH_CHANGE */, onHashChange); | ||
} | ||
function trackLoadingTime(loadType, callback) { | ||
var isWaitingForLoadEvent = loadType === ViewLoadingType.INITIAL_LOAD; | ||
var isWaitingForActivityLoadingTime = true; | ||
var loadingTimeCandidates = []; | ||
function invokeCallbackIfAllCandidatesAreReceived() { | ||
if (!isWaitingForActivityLoadingTime && !isWaitingForLoadEvent && loadingTimeCandidates.length > 0) { | ||
callback(Math.max.apply(Math, loadingTimeCandidates)); | ||
} | ||
} | ||
return { | ||
setLoadEvent: function (loadEvent) { | ||
if (isWaitingForLoadEvent) { | ||
isWaitingForLoadEvent = false; | ||
loadingTimeCandidates.push(loadEvent); | ||
invokeCallbackIfAllCandidatesAreReceived(); | ||
} | ||
}, | ||
setActivityLoadingTime: function (activityLoadingTime) { | ||
if (isWaitingForActivityLoadingTime) { | ||
isWaitingForActivityLoadingTime = false; | ||
if (activityLoadingTime !== undefined) { | ||
loadingTimeCandidates.push(activityLoadingTime); | ||
} | ||
invokeCallbackIfAllCandidatesAreReceived(); | ||
} | ||
}, | ||
}; | ||
} | ||
function trackActivityLoadingTime(lifeCycle, callback) { | ||
var startTime = relativeNow(); | ||
var stopWaitIdlePageActivity = waitIdlePageActivity(lifeCycle, function (hadActivity, endTime) { | ||
if (hadActivity) { | ||
callback(elapsed(startTime, endTime)); | ||
} | ||
else { | ||
callback(undefined); | ||
} | ||
}).stop; | ||
return { stop: stopWaitIdlePageActivity }; | ||
} | ||
/** | ||
* Track layout shifts (LS) occurring during the Views. This yields multiple values that can be | ||
* added up to compute the cumulated layout shift (CLS). | ||
* | ||
* See isLayoutShiftSupported to check for browser support. | ||
* | ||
* Documentation: https://web.dev/cls/ | ||
* Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/master/src/getCLS.ts | ||
*/ | ||
function trackLayoutShift(lifeCycle, callback) { | ||
var stop = lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, function (entry) { | ||
if (entry.entryType === 'layout-shift' && !entry.hadRecentInput) { | ||
callback(entry.value); | ||
} | ||
}).unsubscribe; | ||
return { | ||
stop: stop, | ||
}; | ||
} | ||
/** | ||
* Check whether `layout-shift` is supported by the browser. | ||
*/ | ||
function isLayoutShiftSupported() { | ||
return supportPerformanceTimingEvent('layout-shift'); | ||
} | ||
/** | ||
* Timing name is used as facet path that must contain only letters, digits, or the characters - _ . @ $ | ||
@@ -282,0 +152,0 @@ */ |
{ | ||
"name": "@datadog/browser-rum-core", | ||
"version": "2.7.2", | ||
"version": "2.7.4", | ||
"license": "Apache-2.0", | ||
@@ -15,3 +15,3 @@ "main": "cjs/index.js", | ||
"dependencies": { | ||
"@datadog/browser-core": "2.7.2", | ||
"@datadog/browser-core": "2.7.4", | ||
"tslib": "^1.10.0" | ||
@@ -27,3 +27,3 @@ }, | ||
}, | ||
"gitHead": "8b40b773059b0244b0314ae2fce91ce39985d2c6" | ||
"gitHead": "5127d03aefeb9d9094df3629c5977bd7411363f7" | ||
} |
@@ -7,3 +7,3 @@ import { Context, RelativeTime } from '@datadog/browser-core' | ||
import { AutoAction, AutoActionCreatedEvent } from './rumEventsCollection/action/trackActions' | ||
import { View, ViewCreatedEvent } from './rumEventsCollection/view/trackViews' | ||
import { ViewEvent, ViewCreatedEvent } from './rumEventsCollection/view/trackViews' | ||
@@ -42,3 +42,3 @@ export enum LifeCycleEventType { | ||
notify(eventType: LifeCycleEventType.VIEW_CREATED, data: ViewCreatedEvent): void | ||
notify(eventType: LifeCycleEventType.VIEW_UPDATED, data: View): void | ||
notify(eventType: LifeCycleEventType.VIEW_UPDATED, data: ViewEvent): void | ||
notify( | ||
@@ -86,3 +86,3 @@ eventType: | ||
subscribe(eventType: LifeCycleEventType.VIEW_CREATED, callback: (data: ViewCreatedEvent) => void): Subscription | ||
subscribe(eventType: LifeCycleEventType.VIEW_UPDATED, callback: (data: View) => void): Subscription | ||
subscribe(eventType: LifeCycleEventType.VIEW_UPDATED, callback: (data: ViewEvent) => void): Subscription | ||
subscribe( | ||
@@ -89,0 +89,0 @@ eventType: |
@@ -1,3 +0,2 @@ | ||
import { Context, Duration, RelativeTime } from '../../../../../core/src' | ||
import { RumEvent } from '../../../../../rum/src' | ||
import { Duration, RelativeTime } from '../../../../../core/src' | ||
import { setup, TestSetupBuilder } from '../../../../test/specHelper' | ||
@@ -9,15 +8,6 @@ import { | ||
} from '../../../browser/performanceCollection' | ||
import { RumEventType, ViewLoadingType } from '../../../rawRumEvent.types' | ||
import { ViewLoadingType } from '../../../rawRumEvent.types' | ||
import { LifeCycleEventType } from '../../lifeCycle' | ||
import { | ||
PAGE_ACTIVITY_END_DELAY, | ||
PAGE_ACTIVITY_MAX_DURATION, | ||
PAGE_ACTIVITY_VALIDATION_DELAY, | ||
} from '../../trackPageActivities' | ||
import { THROTTLE_VIEW_UPDATE_PERIOD, trackViews, View, ViewCreatedEvent } from './trackViews' | ||
import { THROTTLE_VIEW_UPDATE_PERIOD, trackViews, ViewEvent, ViewCreatedEvent } from './trackViews' | ||
const AFTER_PAGE_ACTIVITY_MAX_DURATION = PAGE_ACTIVITY_MAX_DURATION * 1.1 | ||
const BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY = (PAGE_ACTIVITY_VALIDATION_DELAY * 0.8) as Duration | ||
const AFTER_PAGE_ACTIVITY_END_DELAY = PAGE_ACTIVITY_END_DELAY * 1.1 | ||
const FAKE_PAINT_ENTRY: RumPerformancePaintTiming = { | ||
@@ -40,23 +30,2 @@ entryType: 'paint', | ||
const FAKE_NAVIGATION_ENTRY_WITH_LOADEVENT_BEFORE_ACTIVITY_TIMING: RumPerformanceNavigationTiming = { | ||
domComplete: 2 as RelativeTime, | ||
domContentLoadedEventEnd: 1 as RelativeTime, | ||
domInteractive: 1 as RelativeTime, | ||
entryType: 'navigation', | ||
loadEventEnd: (BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY * 0.8) as RelativeTime, | ||
} | ||
const FAKE_NAVIGATION_ENTRY_WITH_LOADEVENT_AFTER_ACTIVITY_TIMING: RumPerformanceNavigationTiming = { | ||
domComplete: 2 as RelativeTime, | ||
domContentLoadedEventEnd: 1 as RelativeTime, | ||
domInteractive: 1 as RelativeTime, | ||
entryType: 'navigation', | ||
loadEventEnd: (BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY * 1.2) as RelativeTime, | ||
} | ||
function mockGetElementById() { | ||
const fakeGetElementById = (elementId: string) => ((elementId === 'testHashValue') as any) as HTMLElement | ||
return spyOn(document, 'getElementById').and.callFake(fakeGetElementById) | ||
} | ||
function spyOnViews() { | ||
@@ -66,3 +35,3 @@ const handler = jasmine.createSpy() | ||
function getViewEvent(index: number) { | ||
return handler.calls.argsFor(index)[0] as View | ||
return handler.calls.argsFor(index)[0] as ViewEvent | ||
} | ||
@@ -77,140 +46,2 @@ | ||
describe('rum track url change', () => { | ||
let setupBuilder: TestSetupBuilder | ||
let initialViewId: string | ||
let createSpy: jasmine.Spy<(event: ViewCreatedEvent) => void> | ||
beforeEach(() => { | ||
setupBuilder = setup() | ||
.withFakeLocation('/foo') | ||
.beforeBuild(({ location, lifeCycle }) => { | ||
const subscription = lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, ({ id }) => { | ||
initialViewId = id | ||
subscription.unsubscribe() | ||
}) | ||
return trackViews(location, lifeCycle) | ||
}) | ||
createSpy = jasmine.createSpy('create') | ||
}) | ||
afterEach(() => { | ||
setupBuilder.cleanup() | ||
}) | ||
it('should create new view on path change', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, createSpy) | ||
history.pushState({}, '', '/bar') | ||
expect(createSpy).toHaveBeenCalled() | ||
const viewContext = createSpy.calls.argsFor(0)[0] | ||
expect(viewContext.id).not.toEqual(initialViewId) | ||
}) | ||
it('should create a new view on hash change from history', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, createSpy) | ||
history.pushState({}, '', '/foo#bar') | ||
expect(createSpy).toHaveBeenCalled() | ||
const viewContext = createSpy.calls.argsFor(0)[0] | ||
expect(viewContext.id).not.toEqual(initialViewId) | ||
}) | ||
it('should not create a new view on hash change from history when the hash has kept the same value', () => { | ||
history.pushState({}, '', '/foo#bar') | ||
const { lifeCycle } = setupBuilder.build() | ||
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, createSpy) | ||
history.pushState({}, '', '/foo#bar') | ||
expect(createSpy).not.toHaveBeenCalled() | ||
}) | ||
it('should create a new view on hash change', (done) => { | ||
const { lifeCycle } = setupBuilder.build() | ||
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, createSpy) | ||
function hashchangeCallBack() { | ||
expect(createSpy).toHaveBeenCalled() | ||
const viewContext = createSpy.calls.argsFor(0)[0] | ||
expect(viewContext.id).not.toEqual(initialViewId) | ||
window.removeEventListener('hashchange', hashchangeCallBack) | ||
done() | ||
} | ||
window.addEventListener('hashchange', hashchangeCallBack) | ||
window.location.hash = '#bar' | ||
}) | ||
it('should not create a new view when the hash has kept the same value', (done) => { | ||
history.pushState({}, '', '/foo#bar') | ||
const { lifeCycle } = setupBuilder.build() | ||
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, createSpy) | ||
function hashchangeCallBack() { | ||
expect(createSpy).not.toHaveBeenCalled() | ||
window.removeEventListener('hashchange', hashchangeCallBack) | ||
done() | ||
} | ||
window.addEventListener('hashchange', hashchangeCallBack) | ||
window.location.hash = '#bar' | ||
}) | ||
it('should not create a new view when it is an Anchor navigation', (done) => { | ||
const { lifeCycle } = setupBuilder.build() | ||
mockGetElementById() | ||
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, createSpy) | ||
function hashchangeCallBack() { | ||
expect(createSpy).not.toHaveBeenCalled() | ||
window.removeEventListener('hashchange', hashchangeCallBack) | ||
done() | ||
} | ||
window.addEventListener('hashchange', hashchangeCallBack) | ||
window.location.hash = '#testHashValue' | ||
}) | ||
it('should acknowledge the view location hash change after an Anchor navigation', (done) => { | ||
const { lifeCycle } = setupBuilder.build() | ||
const spyObj = mockGetElementById() | ||
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, createSpy) | ||
function hashchangeCallBack() { | ||
expect(createSpy).not.toHaveBeenCalled() | ||
window.removeEventListener('hashchange', hashchangeCallBack) | ||
// clear mockGetElementById that fake Anchor nav | ||
spyObj.and.callThrough() | ||
// This is not an Anchor nav anymore but the hash and pathname have not been updated | ||
history.pushState({}, '', '/foo#testHashValue') | ||
expect(createSpy).not.toHaveBeenCalled() | ||
done() | ||
} | ||
window.addEventListener('hashchange', hashchangeCallBack) | ||
window.location.hash = '#testHashValue' | ||
}) | ||
it('should not create new view on search change', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
lifeCycle.subscribe(LifeCycleEventType.VIEW_CREATED, createSpy) | ||
history.pushState({}, '', '/foo?bar=qux') | ||
expect(createSpy).not.toHaveBeenCalled() | ||
}) | ||
}) | ||
describe('rum view referrer', () => { | ||
@@ -283,3 +114,3 @@ let setupBuilder: TestSetupBuilder | ||
let getHandledCount: () => number | ||
let getViewEvent: (index: number) => View | ||
let getViewEvent: (index: number) => ViewEvent | ||
@@ -330,3 +161,3 @@ beforeEach(() => { | ||
let handler: jasmine.Spy | ||
let getViewEvent: (index: number) => View | ||
let getViewEvent: (index: number) => ViewEvent | ||
@@ -368,3 +199,3 @@ beforeEach(() => { | ||
let handler: jasmine.Spy | ||
let getViewEvent: (index: number) => View | ||
let getViewEvent: (index: number) => ViewEvent | ||
@@ -405,13 +236,12 @@ beforeEach(() => { | ||
describe('rum track loading time', () => { | ||
describe('rum view timings', () => { | ||
let setupBuilder: TestSetupBuilder | ||
let handler: jasmine.Spy | ||
let getViewEvent: (index: number) => View | ||
let getHandledCount: () => number | ||
let getViewEvent: (index: number) => ViewEvent | ||
beforeEach(() => { | ||
;({ handler, getHandledCount, getViewEvent } = spyOnViews()) | ||
;({ handler, getViewEvent, getHandledCount } = spyOnViews()) | ||
setupBuilder = setup() | ||
.withFakeClock() | ||
.withFakeLocation('/foo') | ||
@@ -428,402 +258,104 @@ .beforeBuild(({ location, lifeCycle }) => { | ||
it('should have an undefined loading time if there is no activity on a route change', () => { | ||
const { clock } = setupBuilder.build() | ||
history.pushState({}, '', '/bar') | ||
clock.tick(AFTER_PAGE_ACTIVITY_MAX_DURATION) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getHandledCount()).toEqual(3) | ||
expect(getViewEvent(2).loadingTime).toBeUndefined() | ||
}) | ||
it('should have a loading time equal to the activity time if there is a unique activity on a route change', () => { | ||
const { lifeCycle, clock } = setupBuilder.build() | ||
history.pushState({}, '', '/bar') | ||
clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY) | ||
lifeCycle.notify(LifeCycleEventType.DOM_MUTATED) | ||
clock.tick(AFTER_PAGE_ACTIVITY_END_DELAY) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getViewEvent(3).loadingTime).toEqual(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY) | ||
}) | ||
it('should use loadEventEnd for initial view when having no activity', () => { | ||
const { lifeCycle, clock } = setupBuilder.build() | ||
it('should update timings when notified with a PERFORMANCE_ENTRY_COLLECTED event (throttled)', () => { | ||
const { lifeCycle, clock } = setupBuilder.withFakeClock().build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).timings).toEqual({}) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_NAVIGATION_ENTRY) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getHandledCount()).toEqual(2) | ||
expect(getViewEvent(1).loadingTime).toEqual(FAKE_NAVIGATION_ENTRY.loadEventEnd) | ||
}) | ||
it('should use loadEventEnd for initial view when load event is bigger than computed loading time', () => { | ||
const { lifeCycle, clock } = setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY) | ||
lifeCycle.notify( | ||
LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, | ||
FAKE_NAVIGATION_ENTRY_WITH_LOADEVENT_AFTER_ACTIVITY_TIMING | ||
) | ||
lifeCycle.notify(LifeCycleEventType.DOM_MUTATED) | ||
clock.tick(AFTER_PAGE_ACTIVITY_END_DELAY) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getHandledCount()).toEqual(2) | ||
expect(getViewEvent(1).loadingTime).toEqual(FAKE_NAVIGATION_ENTRY_WITH_LOADEVENT_AFTER_ACTIVITY_TIMING.loadEventEnd) | ||
expect(getViewEvent(1).timings).toEqual({ | ||
domComplete: 456 as Duration, | ||
domContentLoaded: 345 as Duration, | ||
domInteractive: 234 as Duration, | ||
loadEvent: 567 as Duration, | ||
}) | ||
}) | ||
it('should use computed loading time for initial view when load event is smaller than computed loading time', () => { | ||
const { lifeCycle, clock } = setupBuilder.build() | ||
it('should update timings when ending a view', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).timings).toEqual({}) | ||
clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY) | ||
lifeCycle.notify( | ||
LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, | ||
FAKE_NAVIGATION_ENTRY_WITH_LOADEVENT_BEFORE_ACTIVITY_TIMING | ||
) | ||
lifeCycle.notify(LifeCycleEventType.DOM_MUTATED) | ||
clock.tick(AFTER_PAGE_ACTIVITY_END_DELAY) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_PAINT_ENTRY) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_NAVIGATION_ENTRY) | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getHandledCount()).toEqual(2) | ||
expect(getViewEvent(1).loadingTime).toEqual(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY) | ||
}) | ||
}) | ||
history.pushState({}, '', '/bar') | ||
describe('rum view measures', () => { | ||
let setupBuilder: TestSetupBuilder | ||
let handler: jasmine.Spy | ||
let getHandledCount: () => number | ||
let getViewEvent: (index: number) => View | ||
beforeEach(() => { | ||
;({ handler, getViewEvent, getHandledCount } = spyOnViews()) | ||
setupBuilder = setup() | ||
.withFakeLocation('/foo') | ||
.beforeBuild(({ location, lifeCycle }) => { | ||
lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, handler) | ||
return trackViews(location, lifeCycle) | ||
}) | ||
expect(getHandledCount()).toEqual(3) | ||
expect(getViewEvent(1).timings).toEqual({ | ||
domComplete: 456 as Duration, | ||
domContentLoaded: 345 as Duration, | ||
domInteractive: 234 as Duration, | ||
firstContentfulPaint: 123 as Duration, | ||
largestContentfulPaint: 789 as Duration, | ||
loadEvent: 567 as Duration, | ||
}) | ||
expect(getViewEvent(2).timings).toEqual({}) | ||
}) | ||
afterEach(() => { | ||
setupBuilder.cleanup() | ||
}) | ||
describe('load event happening after initial view end', () => { | ||
let initialView: { init: ViewEvent; end: ViewEvent; last: ViewEvent } | ||
let secondView: { init: ViewEvent; last: ViewEvent } | ||
const VIEW_DURATION = 100 as Duration | ||
describe('timings', () => { | ||
it('should update timings when notified with a PERFORMANCE_ENTRY_COLLECTED event (throttled)', () => { | ||
beforeEach(() => { | ||
const { lifeCycle, clock } = setupBuilder.withFakeClock().build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).timings).toEqual({}) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_NAVIGATION_ENTRY) | ||
clock.tick(VIEW_DURATION) | ||
expect(getHandledCount()).toEqual(1) | ||
history.pushState({}, '', '/bar') | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
clock.tick(VIEW_DURATION) | ||
expect(getHandledCount()).toEqual(2) | ||
expect(getViewEvent(1).timings).toEqual({ | ||
domComplete: 456 as Duration, | ||
domContentLoaded: 345 as Duration, | ||
domInteractive: 234 as Duration, | ||
loadEvent: 567 as Duration, | ||
}) | ||
}) | ||
expect(getHandledCount()).toEqual(3) | ||
it('should update timings when ending a view', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).timings).toEqual({}) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_PAINT_ENTRY) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_NAVIGATION_ENTRY) | ||
expect(getHandledCount()).toEqual(1) | ||
history.pushState({}, '', '/bar') | ||
expect(getHandledCount()).toEqual(3) | ||
expect(getViewEvent(1).timings).toEqual({ | ||
domComplete: 456 as Duration, | ||
domContentLoaded: 345 as Duration, | ||
domInteractive: 234 as Duration, | ||
firstContentfulPaint: 123 as Duration, | ||
largestContentfulPaint: 789 as Duration, | ||
loadEvent: 567 as Duration, | ||
}) | ||
expect(getViewEvent(2).timings).toEqual({}) | ||
}) | ||
describe('load event happening after initial view end', () => { | ||
let initialView: { init: View; end: View; last: View } | ||
let secondView: { init: View; last: View } | ||
const VIEW_DURATION = 100 as Duration | ||
beforeEach(() => { | ||
const { lifeCycle, clock } = setupBuilder.withFakeClock().build() | ||
expect(getHandledCount()).toEqual(1) | ||
clock.tick(VIEW_DURATION) | ||
history.pushState({}, '', '/bar') | ||
clock.tick(VIEW_DURATION) | ||
expect(getHandledCount()).toEqual(3) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_PAINT_ENTRY) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_LARGEST_CONTENTFUL_PAINT_ENTRY) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_NAVIGATION_ENTRY) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getHandledCount()).toEqual(4) | ||
initialView = { | ||
end: getViewEvent(1), | ||
init: getViewEvent(0), | ||
last: getViewEvent(3), | ||
} | ||
secondView = { | ||
init: getViewEvent(2), | ||
last: getViewEvent(2), | ||
} | ||
}) | ||
it('should not set timings to the second view', () => { | ||
expect(secondView.last.timings).toEqual({}) | ||
}) | ||
it('should set timings only on the initial view', () => { | ||
expect(initialView.last.timings).toEqual({ | ||
domComplete: 456 as Duration, | ||
domContentLoaded: 345 as Duration, | ||
domInteractive: 234 as Duration, | ||
firstContentfulPaint: 123 as Duration, | ||
largestContentfulPaint: 789 as Duration, | ||
loadEvent: 567 as Duration, | ||
}) | ||
}) | ||
it('should not update the initial view duration when updating it with new timings', () => { | ||
expect(initialView.end.duration).toBe(VIEW_DURATION) | ||
expect(initialView.last.duration).toBe(VIEW_DURATION) | ||
}) | ||
it('should update the initial view loadingTime following the loadEventEnd value', () => { | ||
expect(initialView.last.loadingTime).toBe(FAKE_NAVIGATION_ENTRY.loadEventEnd) | ||
}) | ||
}) | ||
}) | ||
describe('event counts', () => { | ||
it('should track error count', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).eventCounts.errorCount).toEqual(0) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.ERROR } as RumEvent & Context) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.ERROR } as RumEvent & Context) | ||
history.pushState({}, '', '/bar') | ||
expect(getHandledCount()).toEqual(3) | ||
expect(getViewEvent(1).eventCounts.errorCount).toEqual(2) | ||
expect(getViewEvent(2).eventCounts.errorCount).toEqual(0) | ||
}) | ||
it('should track long task count', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).eventCounts.longTaskCount).toEqual(0) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.LONG_TASK } as RumEvent & Context) | ||
history.pushState({}, '', '/bar') | ||
expect(getHandledCount()).toEqual(3) | ||
expect(getViewEvent(1).eventCounts.longTaskCount).toEqual(1) | ||
expect(getViewEvent(2).eventCounts.longTaskCount).toEqual(0) | ||
}) | ||
it('should track resource count', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).eventCounts.resourceCount).toEqual(0) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.RESOURCE } as RumEvent & Context) | ||
history.pushState({}, '', '/bar') | ||
expect(getHandledCount()).toEqual(3) | ||
expect(getViewEvent(1).eventCounts.resourceCount).toEqual(1) | ||
expect(getViewEvent(2).eventCounts.resourceCount).toEqual(0) | ||
}) | ||
it('should track action count', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).eventCounts.userActionCount).toEqual(0) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.ACTION } as RumEvent & Context) | ||
history.pushState({}, '', '/bar') | ||
expect(getHandledCount()).toEqual(3) | ||
expect(getViewEvent(1).eventCounts.userActionCount).toEqual(1) | ||
expect(getViewEvent(2).eventCounts.userActionCount).toEqual(0) | ||
}) | ||
it('should reset event count when the view changes', () => { | ||
const { lifeCycle } = setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).eventCounts.resourceCount).toEqual(0) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.RESOURCE } as RumEvent & Context) | ||
history.pushState({}, '', '/bar') | ||
expect(getHandledCount()).toEqual(3) | ||
expect(getViewEvent(1).eventCounts.resourceCount).toEqual(1) | ||
expect(getViewEvent(2).eventCounts.resourceCount).toEqual(0) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.RESOURCE } as RumEvent & Context) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.RESOURCE } as RumEvent & Context) | ||
history.pushState({}, '', '/baz') | ||
expect(getHandledCount()).toEqual(5) | ||
expect(getViewEvent(3).eventCounts.resourceCount).toEqual(2) | ||
expect(getViewEvent(4).eventCounts.resourceCount).toEqual(0) | ||
}) | ||
it('should update eventCounts when a resource event is collected (throttled)', () => { | ||
const { lifeCycle, clock } = setupBuilder.withFakeClock().build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).eventCounts).toEqual({ | ||
errorCount: 0, | ||
longTaskCount: 0, | ||
resourceCount: 0, | ||
userActionCount: 0, | ||
}) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.RESOURCE } as RumEvent & Context) | ||
expect(getHandledCount()).toEqual(1) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getHandledCount()).toEqual(2) | ||
expect(getViewEvent(1).eventCounts).toEqual({ | ||
errorCount: 0, | ||
longTaskCount: 0, | ||
resourceCount: 1, | ||
userActionCount: 0, | ||
}) | ||
}) | ||
expect(getHandledCount()).toEqual(4) | ||
it('should not update eventCounts after ending a view', () => { | ||
const { lifeCycle, clock } = setupBuilder.withFakeClock().build() | ||
expect(getHandledCount()).toEqual(1) | ||
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, { type: RumEventType.RESOURCE } as RumEvent & Context) | ||
expect(getHandledCount()).toEqual(1) | ||
history.pushState({}, '', '/bar') | ||
expect(getHandledCount()).toEqual(3) | ||
expect(getViewEvent(1).id).toEqual(getViewEvent(0).id) | ||
expect(getViewEvent(2).id).not.toEqual(getViewEvent(0).id) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getHandledCount()).toEqual(3) | ||
}) | ||
}) | ||
describe('cumulativeLayoutShift', () => { | ||
let isLayoutShiftSupported: boolean | ||
beforeEach(() => { | ||
if (!('PerformanceObserver' in window) || !('supportedEntryTypes' in PerformanceObserver)) { | ||
pending('No PerformanceObserver support') | ||
initialView = { | ||
end: getViewEvent(1), | ||
init: getViewEvent(0), | ||
last: getViewEvent(3), | ||
} | ||
isLayoutShiftSupported = true | ||
spyOnProperty(PerformanceObserver, 'supportedEntryTypes', 'get').and.callFake(() => | ||
isLayoutShiftSupported ? ['layout-shift'] : [] | ||
) | ||
secondView = { | ||
init: getViewEvent(2), | ||
last: getViewEvent(2), | ||
} | ||
}) | ||
it('should be initialized to 0', () => { | ||
setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).cumulativeLayoutShift).toBe(0) | ||
it('should not set timings to the second view', () => { | ||
expect(secondView.last.timings).toEqual({}) | ||
}) | ||
it('should be initialized to undefined if layout-shift is not supported', () => { | ||
isLayoutShiftSupported = false | ||
setupBuilder.build() | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).cumulativeLayoutShift).toBe(undefined) | ||
}) | ||
it('should accumulate layout shift values', () => { | ||
const { lifeCycle, clock } = setupBuilder.withFakeClock().build() | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, { | ||
entryType: 'layout-shift', | ||
hadRecentInput: false, | ||
value: 0.1, | ||
it('should set timings only on the initial view', () => { | ||
expect(initialView.last.timings).toEqual({ | ||
domComplete: 456 as Duration, | ||
domContentLoaded: 345 as Duration, | ||
domInteractive: 234 as Duration, | ||
firstContentfulPaint: 123 as Duration, | ||
largestContentfulPaint: 789 as Duration, | ||
loadEvent: 567 as Duration, | ||
}) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, { | ||
entryType: 'layout-shift', | ||
hadRecentInput: false, | ||
value: 0.2, | ||
}) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getHandledCount()).toEqual(2) | ||
expect(getViewEvent(1).cumulativeLayoutShift).toBe(0.3) | ||
}) | ||
it('should round the cumulative layout shift value to 4 decimals', () => { | ||
const { lifeCycle, clock } = setupBuilder.withFakeClock().build() | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, { | ||
entryType: 'layout-shift', | ||
hadRecentInput: false, | ||
value: 1.23456789, | ||
}) | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, { | ||
entryType: 'layout-shift', | ||
hadRecentInput: false, | ||
value: 1.11111111111, | ||
}) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getHandledCount()).toEqual(2) | ||
expect(getViewEvent(1).cumulativeLayoutShift).toBe(2.3457) | ||
it('should not update the initial view duration when updating it with new timings', () => { | ||
expect(initialView.end.duration).toBe(VIEW_DURATION) | ||
expect(initialView.last.duration).toBe(VIEW_DURATION) | ||
}) | ||
it('should ignore entries with recent input', () => { | ||
const { lifeCycle, clock } = setupBuilder.withFakeClock().build() | ||
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, { | ||
entryType: 'layout-shift', | ||
hadRecentInput: true, | ||
value: 0.1, | ||
}) | ||
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD) | ||
expect(getHandledCount()).toEqual(1) | ||
expect(getViewEvent(0).cumulativeLayoutShift).toBe(0) | ||
it('should update the initial view loadingTime following the loadEventEnd value', () => { | ||
expect(initialView.last.loadingTime).toBe(FAKE_NAVIGATION_ENTRY.loadEventEnd) | ||
}) | ||
@@ -836,3 +368,3 @@ }) | ||
let handler: jasmine.Spy | ||
let getViewEvent: (index: number) => View | ||
let getViewEvent: (index: number) => ViewEvent | ||
let addTiming: (name: string, time?: RelativeTime) => void | ||
@@ -933,3 +465,3 @@ | ||
let handler: jasmine.Spy | ||
let getViewEvent: (index: number) => View | ||
let getViewEvent: (index: number) => ViewEvent | ||
@@ -936,0 +468,0 @@ beforeEach(() => { |
import { | ||
addEventListener, | ||
DOM_EVENT, | ||
Duration, | ||
@@ -8,18 +6,16 @@ elapsed, | ||
monitor, | ||
noop, | ||
ONE_MINUTE, | ||
relativeNow, | ||
RelativeTime, | ||
round, | ||
throttle, | ||
} from '@datadog/browser-core' | ||
import { supportPerformanceTimingEvent } from '../../../browser/performanceCollection' | ||
import { LifeCycle, LifeCycleEventType } from '../../lifeCycle' | ||
import { EventCounts, trackEventCounts } from '../../trackEventCounts' | ||
import { waitIdlePageActivity } from '../../trackPageActivities' | ||
import { EventCounts } from '../../trackEventCounts' | ||
import { ViewLoadingType, ViewCustomTimings } from '../../../rawRumEvent.types' | ||
import { Timings, trackTimings } from './trackTimings' | ||
import { Timings, trackInitialViewTimings } from './trackInitialViewTimings' | ||
import { trackViewMetrics } from './trackViewMetrics' | ||
import { trackLocationChanges, areDifferentLocation } from './trackLocationChanges' | ||
export interface View { | ||
export interface ViewEvent { | ||
id: string | ||
@@ -54,28 +50,12 @@ name?: string | ||
export function trackViews(location: Location, lifeCycle: LifeCycle) { | ||
const startOrigin = 0 as RelativeTime | ||
let hasReplay = false | ||
const initialView = newView( | ||
lifeCycle, | ||
location, | ||
hasReplay, | ||
ViewLoadingType.INITIAL_LOAD, | ||
document.referrer, | ||
startOrigin | ||
) | ||
let currentView = initialView | ||
let isRecording = false | ||
const { stop: stopTimingsTracking } = trackTimings(lifeCycle, (timings) => { | ||
initialView.updateTimings(timings) | ||
initialView.scheduleUpdate() | ||
}) | ||
const { stop: stopHistoryTracking } = trackHistory(onLocationChange) | ||
const { stop: stopHashTracking } = trackHash(onLocationChange) | ||
function onLocationChange() { | ||
if (currentView.isDifferentView(location)) { | ||
// eslint-disable-next-line prefer-const | ||
let { stop: stopInitialViewTracking, initialView: currentView } = trackInitialView() | ||
const { stop: stopLocationChangesTracking } = trackLocationChanges(() => { | ||
if (areDifferentLocation(currentView.getLocation(), location)) { | ||
// Renew view on location changes | ||
currentView.end() | ||
currentView.triggerUpdate() | ||
currentView = newView(lifeCycle, location, hasReplay, ViewLoadingType.ROUTE_CHANGE, currentView.url) | ||
currentView = trackViewChange() | ||
return | ||
@@ -85,3 +65,3 @@ } | ||
currentView.triggerUpdate() | ||
} | ||
}) | ||
@@ -92,3 +72,3 @@ // Renew view on session renewal | ||
currentView.end() | ||
currentView = newView(lifeCycle, location, hasReplay, ViewLoadingType.ROUTE_CHANGE, currentView.url) | ||
currentView = trackViewChange() | ||
}) | ||
@@ -103,3 +83,3 @@ | ||
lifeCycle.subscribe(LifeCycleEventType.RECORD_STARTED, () => { | ||
hasReplay = true | ||
isRecording = true | ||
currentView.updateHasReplay(true) | ||
@@ -109,3 +89,3 @@ }) | ||
lifeCycle.subscribe(LifeCycleEventType.RECORD_STOPPED, () => { | ||
hasReplay = false | ||
isRecording = false | ||
}) | ||
@@ -121,2 +101,23 @@ | ||
function trackInitialView() { | ||
const startOrigin = 0 as RelativeTime | ||
const initialView = newView( | ||
lifeCycle, | ||
location, | ||
isRecording, | ||
ViewLoadingType.INITIAL_LOAD, | ||
document.referrer, | ||
startOrigin | ||
) | ||
const { stop } = trackInitialViewTimings(lifeCycle, (timings) => { | ||
initialView.updateTimings(timings) | ||
initialView.scheduleUpdate() | ||
}) | ||
return { initialView, stop } | ||
} | ||
function trackViewChange() { | ||
return newView(lifeCycle, location, isRecording, ViewLoadingType.ROUTE_CHANGE, currentView.url) | ||
} | ||
return { | ||
@@ -128,5 +129,4 @@ addTiming: (name: string, time = relativeNow()) => { | ||
stop: () => { | ||
stopHistoryTracking() | ||
stopHashTracking() | ||
stopTimingsTracking() | ||
stopInitialViewTracking() | ||
stopLocationChangesTracking() | ||
currentView.end() | ||
@@ -149,13 +149,5 @@ clearInterval(keepAliveInterval) | ||
const id = generateUUID() | ||
let eventCounts: EventCounts = { | ||
errorCount: 0, | ||
longTaskCount: 0, | ||
resourceCount: 0, | ||
userActionCount: 0, | ||
} | ||
let timings: Timings = {} | ||
const customTimings: ViewCustomTimings = {} | ||
let documentVersion = 0 | ||
let cumulativeLayoutShift: number | undefined | ||
let loadingTime: Duration | undefined | ||
let endTime: RelativeTime | undefined | ||
@@ -176,25 +168,8 @@ let location: Location = { ...initialLocation } | ||
const { stop: stopEventCountsTracking } = trackEventCounts(lifeCycle, (newEventCounts) => { | ||
eventCounts = newEventCounts | ||
scheduleViewUpdate() | ||
}) | ||
const { setLoadEvent, stop: stopViewMetricsTracking, viewMetrics } = trackViewMetrics( | ||
lifeCycle, | ||
scheduleViewUpdate, | ||
loadingType | ||
) | ||
const { setActivityLoadingTime, setLoadEvent } = trackLoadingTime(loadingType, (newLoadingTime) => { | ||
loadingTime = newLoadingTime | ||
scheduleViewUpdate() | ||
}) | ||
const { stop: stopActivityLoadingTimeTracking } = trackActivityLoadingTime(lifeCycle, setActivityLoadingTime) | ||
let stopCLSTracking: () => void | ||
if (isLayoutShiftSupported()) { | ||
cumulativeLayoutShift = 0 | ||
;({ stop: stopCLSTracking } = trackLayoutShift(lifeCycle, (layoutShift) => { | ||
cumulativeLayoutShift! += layoutShift | ||
scheduleViewUpdate() | ||
})) | ||
} else { | ||
stopCLSTracking = noop | ||
} | ||
// Initial view update | ||
@@ -206,9 +181,7 @@ triggerViewUpdate() | ||
lifeCycle.notify(LifeCycleEventType.VIEW_UPDATED, { | ||
cumulativeLayoutShift: cumulativeLayoutShift && round(cumulativeLayoutShift, 4), | ||
...viewMetrics, | ||
customTimings, | ||
documentVersion, | ||
eventCounts, | ||
id, | ||
name, | ||
loadingTime, | ||
loadingType, | ||
@@ -229,13 +202,5 @@ location, | ||
endTime = relativeNow() | ||
stopEventCountsTracking() | ||
stopActivityLoadingTimeTracking() | ||
stopCLSTracking() | ||
stopViewMetricsTracking() | ||
lifeCycle.notify(LifeCycleEventType.VIEW_ENDED) | ||
}, | ||
isDifferentView(otherLocation: Location) { | ||
return ( | ||
location.pathname !== otherLocation.pathname || | ||
(!isHashAnAnchor(otherLocation.hash) && otherLocation.hash !== location.hash) | ||
) | ||
}, | ||
getLocation() { | ||
@@ -270,106 +235,3 @@ return location | ||
function isHashAnAnchor(hash: string) { | ||
const correspondingId = hash.substr(1) | ||
return !!document.getElementById(correspondingId) | ||
} | ||
function trackHistory(onHistoryChange: () => void) { | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
const originalPushState = history.pushState | ||
history.pushState = monitor(function (this: History['pushState']) { | ||
originalPushState.apply(this, arguments as any) | ||
onHistoryChange() | ||
}) | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
const originalReplaceState = history.replaceState | ||
history.replaceState = monitor(function (this: History['replaceState']) { | ||
originalReplaceState.apply(this, arguments as any) | ||
onHistoryChange() | ||
}) | ||
const { stop: removeListener } = addEventListener(window, DOM_EVENT.POP_STATE, onHistoryChange) | ||
const stop = () => { | ||
removeListener() | ||
history.pushState = originalPushState | ||
history.replaceState = originalReplaceState | ||
} | ||
return { stop } | ||
} | ||
function trackHash(onHashChange: () => void) { | ||
return addEventListener(window, DOM_EVENT.HASH_CHANGE, onHashChange) | ||
} | ||
function trackLoadingTime(loadType: ViewLoadingType, callback: (loadingTime: Duration) => void) { | ||
let isWaitingForLoadEvent = loadType === ViewLoadingType.INITIAL_LOAD | ||
let isWaitingForActivityLoadingTime = true | ||
const loadingTimeCandidates: Duration[] = [] | ||
function invokeCallbackIfAllCandidatesAreReceived() { | ||
if (!isWaitingForActivityLoadingTime && !isWaitingForLoadEvent && loadingTimeCandidates.length > 0) { | ||
callback(Math.max(...loadingTimeCandidates) as Duration) | ||
} | ||
} | ||
return { | ||
setLoadEvent: (loadEvent: Duration) => { | ||
if (isWaitingForLoadEvent) { | ||
isWaitingForLoadEvent = false | ||
loadingTimeCandidates.push(loadEvent) | ||
invokeCallbackIfAllCandidatesAreReceived() | ||
} | ||
}, | ||
setActivityLoadingTime: (activityLoadingTime: Duration | undefined) => { | ||
if (isWaitingForActivityLoadingTime) { | ||
isWaitingForActivityLoadingTime = false | ||
if (activityLoadingTime !== undefined) { | ||
loadingTimeCandidates.push(activityLoadingTime) | ||
} | ||
invokeCallbackIfAllCandidatesAreReceived() | ||
} | ||
}, | ||
} | ||
} | ||
function trackActivityLoadingTime(lifeCycle: LifeCycle, callback: (loadingTimeValue: Duration | undefined) => void) { | ||
const startTime = relativeNow() | ||
const { stop: stopWaitIdlePageActivity } = waitIdlePageActivity(lifeCycle, (hadActivity, endTime) => { | ||
if (hadActivity) { | ||
callback(elapsed(startTime, endTime)) | ||
} else { | ||
callback(undefined) | ||
} | ||
}) | ||
return { stop: stopWaitIdlePageActivity } | ||
} | ||
/** | ||
* Track layout shifts (LS) occurring during the Views. This yields multiple values that can be | ||
* added up to compute the cumulated layout shift (CLS). | ||
* | ||
* See isLayoutShiftSupported to check for browser support. | ||
* | ||
* Documentation: https://web.dev/cls/ | ||
* Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/master/src/getCLS.ts | ||
*/ | ||
function trackLayoutShift(lifeCycle: LifeCycle, callback: (layoutShift: number) => void) { | ||
const { unsubscribe: stop } = lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, (entry) => { | ||
if (entry.entryType === 'layout-shift' && !entry.hadRecentInput) { | ||
callback(entry.value) | ||
} | ||
}) | ||
return { | ||
stop, | ||
} | ||
} | ||
/** | ||
* Check whether `layout-shift` is supported by the browser. | ||
*/ | ||
function isLayoutShiftSupported() { | ||
return supportPerformanceTimingEvent('layout-shift') | ||
} | ||
/** | ||
* Timing name is used as facet path that must contain only letters, digits, or the characters - _ . @ $ | ||
@@ -376,0 +238,0 @@ */ |
@@ -5,3 +5,3 @@ import { Duration, RelativeTime, ServerDuration } from '@datadog/browser-core' | ||
import { LifeCycleEventType } from '../../lifeCycle' | ||
import { View } from './trackViews' | ||
import { ViewEvent } from './trackViews' | ||
import { startViewCollection } from './viewCollection' | ||
@@ -29,3 +29,3 @@ | ||
const location: Partial<Location> = {} | ||
const view: View = { | ||
const view: ViewEvent = { | ||
cumulativeLayoutShift: 1, | ||
@@ -32,0 +32,0 @@ customTimings: { |
@@ -11,3 +11,3 @@ import { | ||
import { LifeCycle, LifeCycleEventType } from '../../lifeCycle' | ||
import { trackViews, View } from './trackViews' | ||
import { trackViews, ViewEvent } from './trackViews' | ||
@@ -22,3 +22,3 @@ export function startViewCollection(lifeCycle: LifeCycle, location: Location) { | ||
function processViewUpdate(view: View) { | ||
function processViewUpdate(view: ViewEvent) { | ||
const viewEvent: RawRumViewEvent = { | ||
@@ -25,0 +25,0 @@ _dd: { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
785390
261
16184
+ Added@datadog/browser-core@2.7.4(transitive)
- Removed@datadog/browser-core@2.7.2(transitive)
Updated@datadog/browser-core@2.7.4