Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@datadog/browser-rum-core

Package Overview
Dependencies
Maintainers
1
Versions
183
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@datadog/browser-rum-core - npm Package Compare versions

Comparing version 2.7.2 to 2.7.4

cjs/domain/rumEventsCollection/view/trackInitialViewTimings.d.ts

2

cjs/boot/buildEnv.js

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc