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

@datadog/browser-rum

Package Overview
Dependencies
Maintainers
1
Versions
260
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@datadog/browser-rum - npm Package Compare versions

Comparing version 4.29.0 to 4.29.1

cjs/domain/record/shadowRootsController.d.ts

12

cjs/domain/record/mutationObserver.d.ts
import { noop } from '@datadog/browser-core';
import type { RumConfiguration } from '@datadog/browser-rum-core';
import type { MutationCallBack } from './observers';
import type { ShadowRootsController } from './shadowRootsController';
interface RumCharacterDataMutationRecord {

@@ -25,14 +26,7 @@ type: 'characterData';

*/
export declare function startMutationObserver(controller: MutationController, mutationCallback: MutationCallBack, configuration: RumConfiguration): {
export declare function startMutationObserver(mutationCallback: MutationCallBack, configuration: RumConfiguration, shadowRootsController: ShadowRootsController, target: Node): {
stop: typeof noop;
flush: typeof noop;
};
/**
* Controls how mutations are processed, allowing to flush pending mutations.
*/
export declare class MutationController {
private flushListener?;
flush(): void;
onFlush(listener: () => void): void;
}
export declare function sortAddedAndMovedNodes(nodes: Node[]): void;
export {};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sortAddedAndMovedNodes = exports.MutationController = exports.startMutationObserver = void 0;
exports.sortAddedAndMovedNodes = exports.startMutationObserver = void 0;
var browser_core_1 = require("@datadog/browser-core");

@@ -15,12 +15,12 @@ var browser_rum_core_1 = require("@datadog/browser-rum-core");

*/
function startMutationObserver(controller, mutationCallback, configuration) {
function startMutationObserver(mutationCallback, configuration, shadowRootsController, target) {
var MutationObserver = (0, browser_rum_core_1.getMutationObserverConstructor)();
if (!MutationObserver) {
return { stop: browser_core_1.noop };
return { stop: browser_core_1.noop, flush: browser_core_1.noop };
}
var mutationBatch = (0, mutationBatch_1.createMutationBatch)(function (mutations) {
processMutations(mutations.concat(observer.takeRecords()), mutationCallback, configuration);
processMutations(mutations.concat(observer.takeRecords()), mutationCallback, configuration, shadowRootsController, target);
});
var observer = new MutationObserver((0, browser_core_1.monitor)(mutationBatch.addMutations));
observer.observe(document, {
observer.observe(target, {
attributeOldValue: true,

@@ -33,3 +33,2 @@ attributes: true,

});
controller.onFlush(mutationBatch.flush);
return {

@@ -40,22 +39,16 @@ stop: function () {

},
flush: function () {
mutationBatch.flush();
},
};
}
exports.startMutationObserver = startMutationObserver;
/**
* Controls how mutations are processed, allowing to flush pending mutations.
*/
var MutationController = /** @class */ (function () {
function MutationController() {
}
MutationController.prototype.flush = function () {
var _a;
(_a = this.flushListener) === null || _a === void 0 ? void 0 : _a.call(this);
};
MutationController.prototype.onFlush = function (listener) {
this.flushListener = listener;
};
return MutationController;
}());
exports.MutationController = MutationController;
function processMutations(mutations, mutationCallback, configuration) {
function processMutations(mutations, mutationCallback, configuration, shadowRootsController, target) {
mutations
.filter(function (mutation) { return mutation.type === 'childList'; })
.forEach(function (mutation) {
mutation.removedNodes.forEach(function (removedNode) {
traverseRemovedShadowDom(removedNode, shadowRootsController.removeShadowRoot);
});
});
// Discard any mutation with a 'target' node that:

@@ -66,7 +59,7 @@ // * isn't injected in the current document or isn't known/serialized yet: those nodes are likely

var filteredMutations = mutations.filter(function (mutation) {
return document.contains(mutation.target) &&
return target.contains(mutation.target) &&
(0, serializationUtils_1.nodeAndAncestorsHaveSerializedNode)(mutation.target) &&
(0, privacy_1.getNodePrivacyLevel)(mutation.target, configuration.defaultPrivacyLevel) !== constants_1.NodePrivacyLevel.HIDDEN;
});
var _a = processChildListMutations(filteredMutations.filter(function (mutation) { return mutation.type === 'childList'; }), configuration), adds = _a.adds, removes = _a.removes, hasBeenSerialized = _a.hasBeenSerialized;
var _a = processChildListMutations(filteredMutations.filter(function (mutation) { return mutation.type === 'childList'; }), configuration, shadowRootsController), adds = _a.adds, removes = _a.removes, hasBeenSerialized = _a.hasBeenSerialized;
var texts = processCharacterDataMutations(filteredMutations.filter(function (mutation) {

@@ -88,3 +81,3 @@ return mutation.type === 'characterData' && !hasBeenSerialized(mutation.target);

}
function processChildListMutations(mutations, configuration) {
function processChildListMutations(mutations, configuration, shadowRootsController) {
// First, we iterate over mutations to collect:

@@ -146,3 +139,3 @@ //

parentNodePrivacyLevel: parentNodePrivacyLevel,
serializationContext: { status: 2 /* MUTATION */ },
serializationContext: { status: 2 /* MUTATION */, shadowRootsController: shadowRootsController },
configuration: configuration,

@@ -153,5 +146,6 @@ });

}
var parentNode = (0, browser_rum_core_1.getParentNode)(node);
addedNodeMutations.push({
nextId: getNextSibling(node),
parentId: (0, serializationUtils_1.getSerializedNodeId)(node.parentNode),
parentId: (0, serializationUtils_1.getSerializedNodeId)(parentNode),
node: serializedNode,

@@ -204,3 +198,3 @@ });

}
var parentNodePrivacyLevel = (0, privacy_1.getNodePrivacyLevel)(mutation.target.parentNode, configuration.defaultPrivacyLevel);
var parentNodePrivacyLevel = (0, privacy_1.getNodePrivacyLevel)((0, browser_rum_core_1.getParentNode)(mutation.target), configuration.defaultPrivacyLevel);
if (parentNodePrivacyLevel === constants_1.NodePrivacyLevel.HIDDEN || parentNodePrivacyLevel === constants_1.NodePrivacyLevel.IGNORE) {

@@ -292,2 +286,11 @@ continue;

exports.sortAddedAndMovedNodes = sortAddedAndMovedNodes;
function traverseRemovedShadowDom(removedNode, shadowDomRemovedCallback) {
if (!(0, browser_core_1.isExperimentalFeatureEnabled)('record_shadow_dom')) {
return;
}
if ((0, browser_rum_core_1.isNodeShadowHost)(removedNode)) {
shadowDomRemovedCallback(removedNode.shadowRoot);
}
(0, browser_rum_core_1.getChildNodes)(removedNode).forEach(function (child) { return traverseRemovedShadowDom(child, shadowDomRemovedCallback); });
}
//# sourceMappingURL=mutationObserver.js.map
import type { DefaultPrivacyLevel } from '@datadog/browser-core';
import { DOM_EVENT, noop } from '@datadog/browser-core';
import type { LifeCycle, RumConfiguration } from '@datadog/browser-rum-core';
import type { InputState, MousePosition, BrowserMutationPayload, ScrollPosition, StyleSheetRule, ViewportResizeDimension, MediaInteraction, FocusRecord, VisualViewportRecord, FrustrationRecord, BrowserIncrementalSnapshotRecord } from '../../types';
import { IncrementalSource } from '../../types';
import type { MutationController } from './mutationObserver';
import type { ElementsScrollPositions } from './elementsScrollPositions';
import type { ShadowRootsController } from './shadowRootsController';
declare type ListenerHandler = () => void;

@@ -24,3 +25,2 @@ declare type MousemoveCallBack = (p: MousePosition[], source: typeof IncrementalSource.MouseMove | typeof IncrementalSource.TouchMove) => void;

configuration: RumConfiguration;
mutationController: MutationController;
elementsScrollPositions: ElementsScrollPositions;

@@ -38,7 +38,19 @@ mutationCb: MutationCallBack;

frustrationCb: FrustrationCallback;
shadowRootsController: ShadowRootsController;
}
export declare function initObservers(o: ObserverParam): ListenerHandler;
export declare function initInputObserver(cb: InputCallback, defaultPrivacyLevel: DefaultPrivacyLevel): ListenerHandler;
export declare function initObservers(o: ObserverParam): {
stop: ListenerHandler;
flush: ListenerHandler;
};
export declare function initMutationObserver(cb: MutationCallBack, configuration: RumConfiguration, shadowRootsController: ShadowRootsController): {
stop: typeof noop;
flush: typeof noop;
};
declare type InputObserverOptions = {
domEvents?: Array<DOM_EVENT.INPUT | DOM_EVENT.CHANGE>;
target?: Node;
};
export declare function initInputObserver(cb: InputCallback, defaultPrivacyLevel: DefaultPrivacyLevel, { domEvents, target }?: InputObserverOptions): ListenerHandler;
export declare function initStyleSheetObserver(cb: StyleSheetCallback): ListenerHandler;
export declare function initFrustrationObserver(lifeCycle: LifeCycle, frustrationCb: FrustrationCallback): ListenerHandler;
export {};
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.initFrustrationObserver = exports.initStyleSheetObserver = exports.initInputObserver = exports.initObservers = void 0;
exports.initFrustrationObserver = exports.initStyleSheetObserver = exports.initInputObserver = exports.initMutationObserver = exports.initObservers = void 0;
var browser_core_1 = require("@datadog/browser-core");

@@ -26,3 +26,3 @@ var browser_rum_core_1 = require("@datadog/browser-rum-core");

function initObservers(o) {
var mutationHandler = initMutationObserver(o.mutationController, o.mutationCb, o.configuration);
var mutationHandler = initMutationObserver(o.mutationCb, o.configuration, o.shadowRootsController);
var mousemoveHandler = initMoveObserver(o.mousemoveCb);

@@ -38,23 +38,29 @@ var mouseInteractionHandler = initMouseInteractionObserver(o.mouseInteractionCb, o.configuration.defaultPrivacyLevel);

var frustrationHandler = initFrustrationObserver(o.lifeCycle, o.frustrationCb);
return function () {
mutationHandler();
mousemoveHandler();
mouseInteractionHandler();
scrollHandler();
viewportResizeHandler();
inputHandler();
mediaInteractionHandler();
styleSheetObserver();
focusHandler();
visualViewportResizeHandler();
frustrationHandler();
return {
flush: function () {
mutationHandler.flush();
},
stop: function () {
mutationHandler.stop();
mousemoveHandler();
mouseInteractionHandler();
scrollHandler();
viewportResizeHandler();
inputHandler();
mediaInteractionHandler();
styleSheetObserver();
focusHandler();
visualViewportResizeHandler();
frustrationHandler();
},
};
}
exports.initObservers = initObservers;
function initMutationObserver(mutationController, cb, configuration) {
return (0, mutationObserver_1.startMutationObserver)(mutationController, cb, configuration).stop;
function initMutationObserver(cb, configuration, shadowRootsController) {
return (0, mutationObserver_1.startMutationObserver)(cb, configuration, shadowRootsController, document);
}
exports.initMutationObserver = initMutationObserver;
function initMoveObserver(cb) {
var updatePosition = (0, browser_core_1.throttle)((0, browser_core_1.monitor)(function (event) {
var target = event.target;
var target = getEventTarget(event);
if ((0, serializationUtils_1.hasSerializedNode)(target)) {

@@ -96,3 +102,3 @@ var _a = (0, utils_1.isTouchEvent)(event) ? event.changedTouches[0] : event, clientX = _a.clientX, clientY = _a.clientY;

var handler = function (event) {
var target = event.target;
var target = getEventTarget(event);
if ((0, privacy_1.getNodePrivacyLevel)(target, defaultPrivacyLevel) === constants_1.NodePrivacyLevel.HIDDEN || !(0, serializationUtils_1.hasSerializedNode)(target)) {

@@ -123,3 +129,3 @@ return;

var updatePosition = (0, browser_core_1.throttle)((0, browser_core_1.monitor)(function (event) {
var target = event.target;
var target = getEventTarget(event);
if (!target ||

@@ -152,3 +158,4 @@ (0, privacy_1.getNodePrivacyLevel)(target, defaultPrivacyLevel) === constants_1.NodePrivacyLevel.HIDDEN ||

}
function initInputObserver(cb, defaultPrivacyLevel) {
function initInputObserver(cb, defaultPrivacyLevel, _a) {
var _b = _a === void 0 ? {} : _a, _c = _b.domEvents, domEvents = _c === void 0 ? ["input" /* INPUT */, "change" /* CHANGE */] : _c, _d = _b.target, target = _d === void 0 ? document : _d;
var lastInputStateMap = new WeakMap();

@@ -205,7 +212,8 @@ function onElementChange(target) {

}
var stopEventListeners = (0, browser_core_1.addEventListeners)(document, ["input" /* INPUT */, "change" /* CHANGE */], function (event) {
if (event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
event.target instanceof HTMLSelectElement) {
onElementChange(event.target);
var stopEventListeners = (0, browser_core_1.addEventListeners)(target, domEvents, function (event) {
var target = getEventTarget(event);
if (target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement) {
onElementChange(target);
}

@@ -284,3 +292,3 @@ }, {

var handler = function (event) {
var target = event.target;
var target = getEventTarget(event);
if (!target ||

@@ -341,2 +349,10 @@ (0, privacy_1.getNodePrivacyLevel)(target, defaultPrivacyLevel) === constants_1.NodePrivacyLevel.HIDDEN ||

exports.initFrustrationObserver = initFrustrationObserver;
function getEventTarget(event) {
if (event.composed === true &&
(0, browser_rum_core_1.isNodeShadowHost)(event.target) &&
(0, browser_core_1.isExperimentalFeatureEnabled)('record_shadow_dom')) {
return event.composedPath()[0];
}
return event.target;
}
//# sourceMappingURL=observers.js.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.shouldIgnoreElement = exports.getTextContent = exports.censorText = exports.shouldMaskNode = exports.getNodeSelfPrivacyLevel = exports.reducePrivacyLevel = exports.getNodePrivacyLevel = exports.MAX_ATTRIBUTE_VALUE_CHAR_LENGTH = void 0;
var browser_rum_core_1 = require("@datadog/browser-rum-core");
var constants_1 = require("../../constants");

@@ -14,5 +15,4 @@ exports.MAX_ATTRIBUTE_VALUE_CHAR_LENGTH = 100000;

function getNodePrivacyLevel(node, defaultPrivacyLevel) {
var parentNodePrivacyLevel = node.parentNode
? getNodePrivacyLevel(node.parentNode, defaultPrivacyLevel)
: defaultPrivacyLevel;
var parentNode = (0, browser_rum_core_1.getParentNode)(node);
var parentNodePrivacyLevel = parentNode ? getNodePrivacyLevel(parentNode, defaultPrivacyLevel) : defaultPrivacyLevel;
var selfNodePrivacyLevel = getNodeSelfPrivacyLevel(node);

@@ -49,3 +49,3 @@ return reducePrivacyLevel(selfNodePrivacyLevel, parentNodePrivacyLevel);

// Only Element types can have a privacy level set
if (!isElement(node)) {
if (!(0, browser_rum_core_1.isElementNode)(node)) {
return;

@@ -109,3 +109,3 @@ }

case constants_1.NodePrivacyLevel.MASK_USER_INPUT:
return isTextNode(node) ? isFormElement(node.parentNode) : isFormElement(node);
return (0, browser_rum_core_1.isTextNode)(node) ? isFormElement(node.parentNode) : isFormElement(node);
default:

@@ -116,8 +116,2 @@ return false;

exports.shouldMaskNode = shouldMaskNode;
function isElement(node) {
return node.nodeType === node.ELEMENT_NODE;
}
function isTextNode(node) {
return node.nodeType === node.TEXT_NODE;
}
function isFormElement(node) {

@@ -124,0 +118,0 @@ if (!node || node.nodeType !== node.ELEMENT_NODE) {

import type { TimeStamp } from '@datadog/browser-core';
import type { LifeCycle, RumConfiguration } from '@datadog/browser-rum-core';
import type { BrowserRecord } from '../../types';
import type { ShadowRootsController } from './shadowRootsController';
export interface RecordOptions {

@@ -13,3 +14,4 @@ emit?: (record: BrowserRecord) => void;

flushMutations: () => void;
shadowRootsController: ShadowRootsController;
}
export declare function record(options: RecordOptions): RecordAPI;

@@ -9,6 +9,6 @@ "use strict";

var observers_1 = require("./observers");
var mutationObserver_1 = require("./mutationObserver");
var viewports_1 = require("./viewports");
var utils_1 = require("./utils");
var elementsScrollPositions_1 = require("./elementsScrollPositions");
var shadowRootsController_1 = require("./shadowRootsController");
function record(options) {

@@ -20,8 +20,15 @@ var emit = options.emit;

}
var mutationController = new mutationObserver_1.MutationController();
var elementsScrollPositions = (0, elementsScrollPositions_1.createElementsScrollPositions)();
var mutationCb = function (mutation) {
emit((0, utils_1.assembleIncrementalSnapshot)(types_1.IncrementalSource.Mutation, mutation));
};
var inputCb = function (s) { return emit((0, utils_1.assembleIncrementalSnapshot)(types_1.IncrementalSource.Input, s)); };
var shadowRootsController = (0, shadowRootsController_1.initShadowRootsController)(options.configuration, { mutationCb: mutationCb, inputCb: inputCb });
var takeFullSnapshot = function (timestamp, serializationContext) {
if (timestamp === void 0) { timestamp = (0, browser_core_1.timeStampNow)(); }
if (serializationContext === void 0) { serializationContext = { status: 0 /* INITIAL_FULL_SNAPSHOT */, elementsScrollPositions: elementsScrollPositions }; }
mutationController.flush(); // process any pending mutation before taking a full snapshot
if (serializationContext === void 0) { serializationContext = {
status: 0 /* INITIAL_FULL_SNAPSHOT */,
elementsScrollPositions: elementsScrollPositions,
shadowRootsController: shadowRootsController,
}; }
var _a = (0, browser_rum_core_1.getViewportDimension)(), width = _a.width, height = _a.height;

@@ -64,8 +71,7 @@ emit({

takeFullSnapshot();
var stopObservers = (0, observers_1.initObservers)({
var _a = (0, observers_1.initObservers)({
lifeCycle: options.lifeCycle,
configuration: options.configuration,
mutationController: mutationController,
elementsScrollPositions: elementsScrollPositions,
inputCb: function (v) { return emit((0, utils_1.assembleIncrementalSnapshot)(types_1.IncrementalSource.Input, v)); },
inputCb: inputCb,
mediaInteractionCb: function (p) {

@@ -76,3 +82,3 @@ return emit((0, utils_1.assembleIncrementalSnapshot)(types_1.IncrementalSource.MediaInteraction, p));

mousemoveCb: function (positions, source) { return emit((0, utils_1.assembleIncrementalSnapshot)(source, { positions: positions })); },
mutationCb: function (m) { return emit((0, utils_1.assembleIncrementalSnapshot)(types_1.IncrementalSource.Mutation, m)); },
mutationCb: mutationCb,
scrollCb: function (p) { return emit((0, utils_1.assembleIncrementalSnapshot)(types_1.IncrementalSource.Scroll, p)); },

@@ -96,7 +102,17 @@ styleSheetCb: function (r) { return emit((0, utils_1.assembleIncrementalSnapshot)(types_1.IncrementalSource.StyleSheetRule, r)); },

},
});
shadowRootsController: shadowRootsController,
}), stopObservers = _a.stop, flushMutationsFromObservers = _a.flush;
function flushMutations() {
shadowRootsController.flush();
flushMutationsFromObservers();
}
return {
stop: stopObservers,
stop: function () {
shadowRootsController.stop();
stopObservers();
},
takeSubsequentFullSnapshot: function (timestamp) {
return takeFullSnapshot(timestamp, {
flushMutations();
takeFullSnapshot(timestamp, {
shadowRootsController: shadowRootsController,
status: 1 /* SUBSEQUENT_FULL_SNAPSHOT */,

@@ -106,3 +122,4 @@ elementsScrollPositions: elementsScrollPositions,

},
flushMutations: function () { return mutationController.flush(); },
flushMutations: flushMutations,
shadowRootsController: shadowRootsController,
};

@@ -109,0 +126,0 @@ }

@@ -5,2 +5,3 @@ "use strict";

var browser_core_1 = require("@datadog/browser-core");
var browser_rum_core_1 = require("@datadog/browser-rum-core");
var constants_1 = require("../../constants");

@@ -16,6 +17,6 @@ var privacy_1 = require("./privacy");

while (current) {
if (!hasSerializedNode(current)) {
if (!hasSerializedNode(current) && !(0, browser_rum_core_1.isNodeShadowRoot)(current)) {
return false;
}
current = current.parentNode;
current = (0, browser_rum_core_1.getParentNode)(current);
}

@@ -22,0 +23,0 @@ return true;

@@ -5,2 +5,3 @@ import type { RumConfiguration } from '@datadog/browser-rum-core';

import type { ElementsScrollPositions } from './elementsScrollPositions';
import type { ShadowRootsController } from './shadowRootsController';
declare type ParentNodePrivacyLevel = typeof NodePrivacyLevel.ALLOW | typeof NodePrivacyLevel.MASK | typeof NodePrivacyLevel.MASK_USER_INPUT;

@@ -14,8 +15,11 @@ export declare const enum SerializationContextStatus {

status: SerializationContextStatus.MUTATION;
shadowRootsController: ShadowRootsController;
} | {
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT;
elementsScrollPositions: ElementsScrollPositions;
shadowRootsController: ShadowRootsController;
} | {
status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT;
elementsScrollPositions: ElementsScrollPositions;
shadowRootsController: ShadowRootsController;
};

@@ -22,0 +26,0 @@ export interface SerializeOptions {

@@ -40,2 +40,4 @@ "use strict";

return serializeDocumentNode(node, options);
case node.DOCUMENT_FRAGMENT_NODE:
return serializeDocumentFragmentNode(node, options);
case node.DOCUMENT_TYPE_NODE:

@@ -66,2 +68,17 @@ return serializeDocumentTypeNode(node);

}
function serializeDocumentFragmentNode(element, options) {
var childNodes = [];
if (element.childNodes.length) {
childNodes = serializeChildNodes(element, options);
}
var isShadowRoot = (0, browser_rum_core_1.isNodeShadowRoot)(element);
if (isShadowRoot) {
options.serializationContext.shadowRootsController.addShadowRoot(element);
}
return {
type: types_1.NodeType.DocumentFragment,
childNodes: childNodes,
isShadowRoot: isShadowRoot,
};
}
/**

@@ -128,2 +145,8 @@ * Serializing Element nodes involves capturing:

}
if ((0, browser_rum_core_1.isNodeShadowHost)(element) && (0, browser_core_1.isExperimentalFeatureEnabled)('record_shadow_dom')) {
var shadowRoot = serializeNodeWithId(element.shadowRoot, options);
if (shadowRoot !== null) {
childNodes.push(shadowRoot);
}
}
return {

@@ -130,0 +153,0 @@ type: types_1.NodeType.Element,

@@ -50,3 +50,3 @@ /**

*/
export declare type SerializedNode = DocumentNode | DocumentTypeNode | ElementNode | TextNode | CDataNode;
export declare type SerializedNode = DocumentNode | DocumentFragmentNode | DocumentTypeNode | ElementNode | TextNode | CDataNode;
/**

@@ -66,3 +66,3 @@ * Browser-specific. Schema of a Record type which contains mutations of a screen.

*/
export declare type BrowserIncrementalData = BrowserMutationData | MousemoveData | MouseInteractionData | ScrollData | InputData | MediaInteractionData | StyleSheetRuleData | ViewportResizeData;
export declare type BrowserIncrementalData = BrowserMutationData | MousemoveData | MouseInteractionData | ScrollData | InputData | MediaInteractionData | StyleSheetRuleData | ViewportResizeData | PointerInteractionData;
/**

@@ -171,2 +171,11 @@ * Browser-specific. Schema of a MutationData.

/**
* Schema of a PointerInteractionData.
*/
export declare type PointerInteractionData = {
/**
* The source of this type of incremental data.
*/
readonly source: 9;
} & PointerInteraction;
/**
* Schema of a Record which contains the screen properties.

@@ -357,2 +366,16 @@ */

/**
* Schema of a Document FragmentNode.
*/
export interface DocumentFragmentNode {
/**
* The type of this Node.
*/
readonly type: 11;
/**
* Is this node a shadow root or not
*/
readonly isShadowRoot: boolean;
childNodes: SerializedNodeWithId[];
}
/**
* Schema of a Document Type Node.

@@ -396,6 +419,2 @@ */

isSVG?: true;
/**
* Is this node a host of a shadow root
*/
isShadowHost?: true;
}

@@ -622,1 +641,26 @@ /**

}
/**
* Schema of a PointerInteraction.
*/
export interface PointerInteraction {
/**
* Schema of an PointerEventType
*/
readonly pointerEventType: 'down' | 'up' | 'move';
/**
* Schema of an PointerType
*/
readonly pointerType: 'mouse' | 'touch' | 'pen';
/**
* Id of the pointer of this PointerInteraction.
*/
pointerId: number;
/**
* X-axis coordinate for this PointerInteraction.
*/
x: number;
/**
* Y-axis coordinate for this PointerInteraction.
*/
y: number;
}

@@ -18,2 +18,3 @@ import type * as SessionReplay from './sessionReplay';

CDATA: SessionReplay.CDataNode['type'];
DocumentFragment: SessionReplay.DocumentFragmentNode['type'];
};

@@ -20,0 +21,0 @@ export declare type NodeType = typeof NodeType[keyof typeof NodeType];

@@ -19,2 +19,3 @@ "use strict";

CDATA: 4,
DocumentFragment: 11,
};

@@ -21,0 +22,0 @@ exports.IncrementalSource = {

import { noop } from '@datadog/browser-core';
import type { RumConfiguration } from '@datadog/browser-rum-core';
import type { MutationCallBack } from './observers';
import type { ShadowRootsController } from './shadowRootsController';
interface RumCharacterDataMutationRecord {

@@ -25,14 +26,7 @@ type: 'characterData';

*/
export declare function startMutationObserver(controller: MutationController, mutationCallback: MutationCallBack, configuration: RumConfiguration): {
export declare function startMutationObserver(mutationCallback: MutationCallBack, configuration: RumConfiguration, shadowRootsController: ShadowRootsController, target: Node): {
stop: typeof noop;
flush: typeof noop;
};
/**
* Controls how mutations are processed, allowing to flush pending mutations.
*/
export declare class MutationController {
private flushListener?;
flush(): void;
onFlush(listener: () => void): void;
}
export declare function sortAddedAndMovedNodes(nodes: Node[]): void;
export {};

@@ -1,3 +0,3 @@

import { monitor, noop } from '@datadog/browser-core';
import { getMutationObserverConstructor } from '@datadog/browser-rum-core';
import { isExperimentalFeatureEnabled, monitor, noop } from '@datadog/browser-core';
import { getChildNodes, isNodeShadowHost, getMutationObserverConstructor, getParentNode, } from '@datadog/browser-rum-core';
import { NodePrivacyLevel } from '../../constants';

@@ -12,12 +12,12 @@ import { getNodePrivacyLevel, getTextContent } from './privacy';

*/
export function startMutationObserver(controller, mutationCallback, configuration) {
export function startMutationObserver(mutationCallback, configuration, shadowRootsController, target) {
var MutationObserver = getMutationObserverConstructor();
if (!MutationObserver) {
return { stop: noop };
return { stop: noop, flush: noop };
}
var mutationBatch = createMutationBatch(function (mutations) {
processMutations(mutations.concat(observer.takeRecords()), mutationCallback, configuration);
processMutations(mutations.concat(observer.takeRecords()), mutationCallback, configuration, shadowRootsController, target);
});
var observer = new MutationObserver(monitor(mutationBatch.addMutations));
observer.observe(document, {
observer.observe(target, {
attributeOldValue: true,

@@ -30,3 +30,2 @@ attributes: true,

});
controller.onFlush(mutationBatch.flush);
return {

@@ -37,21 +36,15 @@ stop: function () {

},
flush: function () {
mutationBatch.flush();
},
};
}
/**
* Controls how mutations are processed, allowing to flush pending mutations.
*/
var MutationController = /** @class */ (function () {
function MutationController() {
}
MutationController.prototype.flush = function () {
var _a;
(_a = this.flushListener) === null || _a === void 0 ? void 0 : _a.call(this);
};
MutationController.prototype.onFlush = function (listener) {
this.flushListener = listener;
};
return MutationController;
}());
export { MutationController };
function processMutations(mutations, mutationCallback, configuration) {
function processMutations(mutations, mutationCallback, configuration, shadowRootsController, target) {
mutations
.filter(function (mutation) { return mutation.type === 'childList'; })
.forEach(function (mutation) {
mutation.removedNodes.forEach(function (removedNode) {
traverseRemovedShadowDom(removedNode, shadowRootsController.removeShadowRoot);
});
});
// Discard any mutation with a 'target' node that:

@@ -62,7 +55,7 @@ // * isn't injected in the current document or isn't known/serialized yet: those nodes are likely

var filteredMutations = mutations.filter(function (mutation) {
return document.contains(mutation.target) &&
return target.contains(mutation.target) &&
nodeAndAncestorsHaveSerializedNode(mutation.target) &&
getNodePrivacyLevel(mutation.target, configuration.defaultPrivacyLevel) !== NodePrivacyLevel.HIDDEN;
});
var _a = processChildListMutations(filteredMutations.filter(function (mutation) { return mutation.type === 'childList'; }), configuration), adds = _a.adds, removes = _a.removes, hasBeenSerialized = _a.hasBeenSerialized;
var _a = processChildListMutations(filteredMutations.filter(function (mutation) { return mutation.type === 'childList'; }), configuration, shadowRootsController), adds = _a.adds, removes = _a.removes, hasBeenSerialized = _a.hasBeenSerialized;
var texts = processCharacterDataMutations(filteredMutations.filter(function (mutation) {

@@ -84,3 +77,3 @@ return mutation.type === 'characterData' && !hasBeenSerialized(mutation.target);

}
function processChildListMutations(mutations, configuration) {
function processChildListMutations(mutations, configuration, shadowRootsController) {
// First, we iterate over mutations to collect:

@@ -142,3 +135,3 @@ //

parentNodePrivacyLevel: parentNodePrivacyLevel,
serializationContext: { status: 2 /* MUTATION */ },
serializationContext: { status: 2 /* MUTATION */, shadowRootsController: shadowRootsController },
configuration: configuration,

@@ -149,5 +142,6 @@ });

}
var parentNode = getParentNode(node);
addedNodeMutations.push({
nextId: getNextSibling(node),
parentId: getSerializedNodeId(node.parentNode),
parentId: getSerializedNodeId(parentNode),
node: serializedNode,

@@ -200,3 +194,3 @@ });

}
var parentNodePrivacyLevel = getNodePrivacyLevel(mutation.target.parentNode, configuration.defaultPrivacyLevel);
var parentNodePrivacyLevel = getNodePrivacyLevel(getParentNode(mutation.target), configuration.defaultPrivacyLevel);
if (parentNodePrivacyLevel === NodePrivacyLevel.HIDDEN || parentNodePrivacyLevel === NodePrivacyLevel.IGNORE) {

@@ -287,2 +281,11 @@ continue;

}
function traverseRemovedShadowDom(removedNode, shadowDomRemovedCallback) {
if (!isExperimentalFeatureEnabled('record_shadow_dom')) {
return;
}
if (isNodeShadowHost(removedNode)) {
shadowDomRemovedCallback(removedNode.shadowRoot);
}
getChildNodes(removedNode).forEach(function (child) { return traverseRemovedShadowDom(child, shadowDomRemovedCallback); });
}
//# sourceMappingURL=mutationObserver.js.map
import type { DefaultPrivacyLevel } from '@datadog/browser-core';
import { DOM_EVENT, noop } from '@datadog/browser-core';
import type { LifeCycle, RumConfiguration } from '@datadog/browser-rum-core';
import type { InputState, MousePosition, BrowserMutationPayload, ScrollPosition, StyleSheetRule, ViewportResizeDimension, MediaInteraction, FocusRecord, VisualViewportRecord, FrustrationRecord, BrowserIncrementalSnapshotRecord } from '../../types';
import { IncrementalSource } from '../../types';
import type { MutationController } from './mutationObserver';
import type { ElementsScrollPositions } from './elementsScrollPositions';
import type { ShadowRootsController } from './shadowRootsController';
declare type ListenerHandler = () => void;

@@ -24,3 +25,2 @@ declare type MousemoveCallBack = (p: MousePosition[], source: typeof IncrementalSource.MouseMove | typeof IncrementalSource.TouchMove) => void;

configuration: RumConfiguration;
mutationController: MutationController;
elementsScrollPositions: ElementsScrollPositions;

@@ -38,7 +38,19 @@ mutationCb: MutationCallBack;

frustrationCb: FrustrationCallback;
shadowRootsController: ShadowRootsController;
}
export declare function initObservers(o: ObserverParam): ListenerHandler;
export declare function initInputObserver(cb: InputCallback, defaultPrivacyLevel: DefaultPrivacyLevel): ListenerHandler;
export declare function initObservers(o: ObserverParam): {
stop: ListenerHandler;
flush: ListenerHandler;
};
export declare function initMutationObserver(cb: MutationCallBack, configuration: RumConfiguration, shadowRootsController: ShadowRootsController): {
stop: typeof noop;
flush: typeof noop;
};
declare type InputObserverOptions = {
domEvents?: Array<DOM_EVENT.INPUT | DOM_EVENT.CHANGE>;
target?: Node;
};
export declare function initInputObserver(cb: InputCallback, defaultPrivacyLevel: DefaultPrivacyLevel, { domEvents, target }?: InputObserverOptions): ListenerHandler;
export declare function initStyleSheetObserver(cb: StyleSheetCallback): ListenerHandler;
export declare function initFrustrationObserver(lifeCycle: LifeCycle, frustrationCb: FrustrationCallback): ListenerHandler;
export {};
var _a;
import { instrumentSetter, instrumentMethodAndCallOriginal, assign, monitor, throttle, addEventListeners, addEventListener, noop, } from '@datadog/browser-core';
import { initViewportObservable } from '@datadog/browser-rum-core';
import { instrumentSetter, instrumentMethodAndCallOriginal, assign, monitor, throttle, addEventListeners, addEventListener, noop, isExperimentalFeatureEnabled, } from '@datadog/browser-core';
import { isNodeShadowHost, initViewportObservable, } from '@datadog/browser-rum-core';
import { NodePrivacyLevel } from '../../constants';

@@ -23,3 +23,3 @@ import { RecordType, IncrementalSource, MediaInteractionType, MouseInteractionType } from '../../types';

export function initObservers(o) {
var mutationHandler = initMutationObserver(o.mutationController, o.mutationCb, o.configuration);
var mutationHandler = initMutationObserver(o.mutationCb, o.configuration, o.shadowRootsController);
var mousemoveHandler = initMoveObserver(o.mousemoveCb);

@@ -35,22 +35,27 @@ var mouseInteractionHandler = initMouseInteractionObserver(o.mouseInteractionCb, o.configuration.defaultPrivacyLevel);

var frustrationHandler = initFrustrationObserver(o.lifeCycle, o.frustrationCb);
return function () {
mutationHandler();
mousemoveHandler();
mouseInteractionHandler();
scrollHandler();
viewportResizeHandler();
inputHandler();
mediaInteractionHandler();
styleSheetObserver();
focusHandler();
visualViewportResizeHandler();
frustrationHandler();
return {
flush: function () {
mutationHandler.flush();
},
stop: function () {
mutationHandler.stop();
mousemoveHandler();
mouseInteractionHandler();
scrollHandler();
viewportResizeHandler();
inputHandler();
mediaInteractionHandler();
styleSheetObserver();
focusHandler();
visualViewportResizeHandler();
frustrationHandler();
},
};
}
function initMutationObserver(mutationController, cb, configuration) {
return startMutationObserver(mutationController, cb, configuration).stop;
export function initMutationObserver(cb, configuration, shadowRootsController) {
return startMutationObserver(cb, configuration, shadowRootsController, document);
}
function initMoveObserver(cb) {
var updatePosition = throttle(monitor(function (event) {
var target = event.target;
var target = getEventTarget(event);
if (hasSerializedNode(target)) {

@@ -92,3 +97,3 @@ var _a = isTouchEvent(event) ? event.changedTouches[0] : event, clientX = _a.clientX, clientY = _a.clientY;

var handler = function (event) {
var target = event.target;
var target = getEventTarget(event);
if (getNodePrivacyLevel(target, defaultPrivacyLevel) === NodePrivacyLevel.HIDDEN || !hasSerializedNode(target)) {

@@ -119,3 +124,3 @@ return;

var updatePosition = throttle(monitor(function (event) {
var target = event.target;
var target = getEventTarget(event);
if (!target ||

@@ -148,3 +153,4 @@ getNodePrivacyLevel(target, defaultPrivacyLevel) === NodePrivacyLevel.HIDDEN ||

}
export function initInputObserver(cb, defaultPrivacyLevel) {
export function initInputObserver(cb, defaultPrivacyLevel, _a) {
var _b = _a === void 0 ? {} : _a, _c = _b.domEvents, domEvents = _c === void 0 ? ["input" /* INPUT */, "change" /* CHANGE */] : _c, _d = _b.target, target = _d === void 0 ? document : _d;
var lastInputStateMap = new WeakMap();

@@ -201,7 +207,8 @@ function onElementChange(target) {

}
var stopEventListeners = addEventListeners(document, ["input" /* INPUT */, "change" /* CHANGE */], function (event) {
if (event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
event.target instanceof HTMLSelectElement) {
onElementChange(event.target);
var stopEventListeners = addEventListeners(target, domEvents, function (event) {
var target = getEventTarget(event);
if (target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement) {
onElementChange(target);
}

@@ -278,3 +285,3 @@ }, {

var handler = function (event) {
var target = event.target;
var target = getEventTarget(event);
if (!target ||

@@ -334,2 +341,10 @@ getNodePrivacyLevel(target, defaultPrivacyLevel) === NodePrivacyLevel.HIDDEN ||

}
function getEventTarget(event) {
if (event.composed === true &&
isNodeShadowHost(event.target) &&
isExperimentalFeatureEnabled('record_shadow_dom')) {
return event.composedPath()[0];
}
return event.target;
}
//# sourceMappingURL=observers.js.map

@@ -0,1 +1,2 @@

import { isElementNode, getParentNode, isTextNode } from '@datadog/browser-rum-core';
import { NodePrivacyLevel, PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_ALLOW, PRIVACY_ATTR_VALUE_MASK, PRIVACY_ATTR_VALUE_MASK_USER_INPUT, PRIVACY_ATTR_VALUE_HIDDEN, PRIVACY_CLASS_ALLOW, PRIVACY_CLASS_MASK, PRIVACY_CLASS_MASK_USER_INPUT, PRIVACY_CLASS_HIDDEN, FORM_PRIVATE_TAG_NAMES, CENSORED_STRING_MARK, } from '../../constants';

@@ -11,5 +12,4 @@ export var MAX_ATTRIBUTE_VALUE_CHAR_LENGTH = 100000;

export function getNodePrivacyLevel(node, defaultPrivacyLevel) {
var parentNodePrivacyLevel = node.parentNode
? getNodePrivacyLevel(node.parentNode, defaultPrivacyLevel)
: defaultPrivacyLevel;
var parentNode = getParentNode(node);
var parentNodePrivacyLevel = parentNode ? getNodePrivacyLevel(parentNode, defaultPrivacyLevel) : defaultPrivacyLevel;
var selfNodePrivacyLevel = getNodeSelfPrivacyLevel(node);

@@ -44,3 +44,3 @@ return reducePrivacyLevel(selfNodePrivacyLevel, parentNodePrivacyLevel);

// Only Element types can have a privacy level set
if (!isElement(node)) {
if (!isElementNode(node)) {
return;

@@ -108,8 +108,2 @@ }

}
function isElement(node) {
return node.nodeType === node.ELEMENT_NODE;
}
function isTextNode(node) {
return node.nodeType === node.TEXT_NODE;
}
function isFormElement(node) {

@@ -116,0 +110,0 @@ if (!node || node.nodeType !== node.ELEMENT_NODE) {

import type { TimeStamp } from '@datadog/browser-core';
import type { LifeCycle, RumConfiguration } from '@datadog/browser-rum-core';
import type { BrowserRecord } from '../../types';
import type { ShadowRootsController } from './shadowRootsController';
export interface RecordOptions {

@@ -13,3 +14,4 @@ emit?: (record: BrowserRecord) => void;

flushMutations: () => void;
shadowRootsController: ShadowRootsController;
}
export declare function record(options: RecordOptions): RecordAPI;

@@ -6,6 +6,6 @@ import { timeStampNow } from '@datadog/browser-core';

import { initObservers } from './observers';
import { MutationController } from './mutationObserver';
import { getVisualViewport, getScrollX, getScrollY } from './viewports';
import { assembleIncrementalSnapshot } from './utils';
import { createElementsScrollPositions } from './elementsScrollPositions';
import { initShadowRootsController } from './shadowRootsController';
export function record(options) {

@@ -17,8 +17,15 @@ var emit = options.emit;

}
var mutationController = new MutationController();
var elementsScrollPositions = createElementsScrollPositions();
var mutationCb = function (mutation) {
emit(assembleIncrementalSnapshot(IncrementalSource.Mutation, mutation));
};
var inputCb = function (s) { return emit(assembleIncrementalSnapshot(IncrementalSource.Input, s)); };
var shadowRootsController = initShadowRootsController(options.configuration, { mutationCb: mutationCb, inputCb: inputCb });
var takeFullSnapshot = function (timestamp, serializationContext) {
if (timestamp === void 0) { timestamp = timeStampNow(); }
if (serializationContext === void 0) { serializationContext = { status: 0 /* INITIAL_FULL_SNAPSHOT */, elementsScrollPositions: elementsScrollPositions }; }
mutationController.flush(); // process any pending mutation before taking a full snapshot
if (serializationContext === void 0) { serializationContext = {
status: 0 /* INITIAL_FULL_SNAPSHOT */,
elementsScrollPositions: elementsScrollPositions,
shadowRootsController: shadowRootsController,
}; }
var _a = getViewportDimension(), width = _a.width, height = _a.height;

@@ -61,8 +68,7 @@ emit({

takeFullSnapshot();
var stopObservers = initObservers({
var _a = initObservers({
lifeCycle: options.lifeCycle,
configuration: options.configuration,
mutationController: mutationController,
elementsScrollPositions: elementsScrollPositions,
inputCb: function (v) { return emit(assembleIncrementalSnapshot(IncrementalSource.Input, v)); },
inputCb: inputCb,
mediaInteractionCb: function (p) {

@@ -73,3 +79,3 @@ return emit(assembleIncrementalSnapshot(IncrementalSource.MediaInteraction, p));

mousemoveCb: function (positions, source) { return emit(assembleIncrementalSnapshot(source, { positions: positions })); },
mutationCb: function (m) { return emit(assembleIncrementalSnapshot(IncrementalSource.Mutation, m)); },
mutationCb: mutationCb,
scrollCb: function (p) { return emit(assembleIncrementalSnapshot(IncrementalSource.Scroll, p)); },

@@ -93,7 +99,17 @@ styleSheetCb: function (r) { return emit(assembleIncrementalSnapshot(IncrementalSource.StyleSheetRule, r)); },

},
});
shadowRootsController: shadowRootsController,
}), stopObservers = _a.stop, flushMutationsFromObservers = _a.flush;
function flushMutations() {
shadowRootsController.flush();
flushMutationsFromObservers();
}
return {
stop: stopObservers,
stop: function () {
shadowRootsController.stop();
stopObservers();
},
takeSubsequentFullSnapshot: function (timestamp) {
return takeFullSnapshot(timestamp, {
flushMutations();
takeFullSnapshot(timestamp, {
shadowRootsController: shadowRootsController,
status: 1 /* SUBSEQUENT_FULL_SNAPSHOT */,

@@ -103,5 +119,6 @@ elementsScrollPositions: elementsScrollPositions,

},
flushMutations: function () { return mutationController.flush(); },
flushMutations: flushMutations,
shadowRootsController: shadowRootsController,
};
}
//# sourceMappingURL=record.js.map
import { buildUrl } from '@datadog/browser-core';
import { getParentNode, isNodeShadowRoot } from '@datadog/browser-rum-core';
import { CENSORED_STRING_MARK } from '../../constants';

@@ -11,6 +12,6 @@ import { shouldMaskNode } from './privacy';

while (current) {
if (!hasSerializedNode(current)) {
if (!hasSerializedNode(current) && !isNodeShadowRoot(current)) {
return false;
}
current = current.parentNode;
current = getParentNode(current);
}

@@ -17,0 +18,0 @@ return true;

@@ -5,2 +5,3 @@ import type { RumConfiguration } from '@datadog/browser-rum-core';

import type { ElementsScrollPositions } from './elementsScrollPositions';
import type { ShadowRootsController } from './shadowRootsController';
declare type ParentNodePrivacyLevel = typeof NodePrivacyLevel.ALLOW | typeof NodePrivacyLevel.MASK | typeof NodePrivacyLevel.MASK_USER_INPUT;

@@ -14,8 +15,11 @@ export declare const enum SerializationContextStatus {

status: SerializationContextStatus.MUTATION;
shadowRootsController: ShadowRootsController;
} | {
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT;
elementsScrollPositions: ElementsScrollPositions;
shadowRootsController: ShadowRootsController;
} | {
status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT;
elementsScrollPositions: ElementsScrollPositions;
shadowRootsController: ShadowRootsController;
};

@@ -22,0 +26,0 @@ export interface SerializeOptions {

@@ -1,3 +0,3 @@

import { assign, startsWith } from '@datadog/browser-core';
import { STABLE_ATTRIBUTES } from '@datadog/browser-rum-core';
import { assign, isExperimentalFeatureEnabled, startsWith } from '@datadog/browser-core';
import { isNodeShadowHost, isNodeShadowRoot, STABLE_ATTRIBUTES } from '@datadog/browser-rum-core';
import { NodePrivacyLevel, PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_HIDDEN, CENSORED_STRING_MARK, CENSORED_IMG_MARK, } from '../../constants';

@@ -35,2 +35,4 @@ import { NodeType } from '../../types';

return serializeDocumentNode(node, options);
case node.DOCUMENT_FRAGMENT_NODE:
return serializeDocumentFragmentNode(node, options);
case node.DOCUMENT_TYPE_NODE:

@@ -60,2 +62,17 @@ return serializeDocumentTypeNode(node);

}
function serializeDocumentFragmentNode(element, options) {
var childNodes = [];
if (element.childNodes.length) {
childNodes = serializeChildNodes(element, options);
}
var isShadowRoot = isNodeShadowRoot(element);
if (isShadowRoot) {
options.serializationContext.shadowRootsController.addShadowRoot(element);
}
return {
type: NodeType.DocumentFragment,
childNodes: childNodes,
isShadowRoot: isShadowRoot,
};
}
/**

@@ -122,2 +139,8 @@ * Serializing Element nodes involves capturing:

}
if (isNodeShadowHost(element) && isExperimentalFeatureEnabled('record_shadow_dom')) {
var shadowRoot = serializeNodeWithId(element.shadowRoot, options);
if (shadowRoot !== null) {
childNodes.push(shadowRoot);
}
}
return {

@@ -124,0 +147,0 @@ type: NodeType.Element,

@@ -50,3 +50,3 @@ /**

*/
export declare type SerializedNode = DocumentNode | DocumentTypeNode | ElementNode | TextNode | CDataNode;
export declare type SerializedNode = DocumentNode | DocumentFragmentNode | DocumentTypeNode | ElementNode | TextNode | CDataNode;
/**

@@ -66,3 +66,3 @@ * Browser-specific. Schema of a Record type which contains mutations of a screen.

*/
export declare type BrowserIncrementalData = BrowserMutationData | MousemoveData | MouseInteractionData | ScrollData | InputData | MediaInteractionData | StyleSheetRuleData | ViewportResizeData;
export declare type BrowserIncrementalData = BrowserMutationData | MousemoveData | MouseInteractionData | ScrollData | InputData | MediaInteractionData | StyleSheetRuleData | ViewportResizeData | PointerInteractionData;
/**

@@ -171,2 +171,11 @@ * Browser-specific. Schema of a MutationData.

/**
* Schema of a PointerInteractionData.
*/
export declare type PointerInteractionData = {
/**
* The source of this type of incremental data.
*/
readonly source: 9;
} & PointerInteraction;
/**
* Schema of a Record which contains the screen properties.

@@ -357,2 +366,16 @@ */

/**
* Schema of a Document FragmentNode.
*/
export interface DocumentFragmentNode {
/**
* The type of this Node.
*/
readonly type: 11;
/**
* Is this node a shadow root or not
*/
readonly isShadowRoot: boolean;
childNodes: SerializedNodeWithId[];
}
/**
* Schema of a Document Type Node.

@@ -396,6 +419,2 @@ */

isSVG?: true;
/**
* Is this node a host of a shadow root
*/
isShadowHost?: true;
}

@@ -622,1 +641,26 @@ /**

}
/**
* Schema of a PointerInteraction.
*/
export interface PointerInteraction {
/**
* Schema of an PointerEventType
*/
readonly pointerEventType: 'down' | 'up' | 'move';
/**
* Schema of an PointerType
*/
readonly pointerType: 'mouse' | 'touch' | 'pen';
/**
* Id of the pointer of this PointerInteraction.
*/
pointerId: number;
/**
* X-axis coordinate for this PointerInteraction.
*/
x: number;
/**
* Y-axis coordinate for this PointerInteraction.
*/
y: number;
}

@@ -18,2 +18,3 @@ import type * as SessionReplay from './sessionReplay';

CDATA: SessionReplay.CDataNode['type'];
DocumentFragment: SessionReplay.DocumentFragmentNode['type'];
};

@@ -20,0 +21,0 @@ export declare type NodeType = typeof NodeType[keyof typeof NodeType];

@@ -16,2 +16,3 @@ export var RecordType = {

CDATA: 4,
DocumentFragment: 11,
};

@@ -18,0 +19,0 @@ export var IncrementalSource = {

{
"name": "@datadog/browser-rum",
"version": "4.29.0",
"version": "4.29.1",
"license": "Apache-2.0",

@@ -15,7 +15,7 @@ "main": "cjs/entries/main.js",

"dependencies": {
"@datadog/browser-core": "4.29.0",
"@datadog/browser-rum-core": "4.29.0"
"@datadog/browser-core": "4.29.1",
"@datadog/browser-rum-core": "4.29.1"
},
"peerDependencies": {
"@datadog/browser-logs": "4.29.0"
"@datadog/browser-logs": "4.29.1"
},

@@ -36,3 +36,3 @@ "peerDependenciesMeta": {

},
"gitHead": "8983972b924a04cbd3084d569673d8e1eaaf3d06"
"gitHead": "14b6b0d10999b8ab3b85c113da4e0399cb064295"
}

@@ -52,3 +52,3 @@ import type { TimeStamp, HttpRequest } from '@datadog/browser-core'

findView() {
return { id: viewId }
return { id: viewId, documentVersion: 0 }
},

@@ -55,0 +55,0 @@ })

@@ -1,2 +0,2 @@

import { DefaultPrivacyLevel, isIE } from '@datadog/browser-core'
import { DefaultPrivacyLevel, isIE, noop, updateExperimentalFeatures } from '@datadog/browser-core'
import type { RumConfiguration } from '@datadog/browser-rum-core'

@@ -14,20 +14,40 @@ import { collectAsyncCalls, createMutationPayloadValidator } from '../../../test/utils'

import { serializeDocument, SerializationContextStatus } from './serialize'
import { sortAddedAndMovedNodes, startMutationObserver, MutationController } from './mutationObserver'
import { sortAddedAndMovedNodes, startMutationObserver } from './mutationObserver'
import type { MutationCallBack } from './observers'
import { createElementsScrollPositions } from './elementsScrollPositions'
import type { ShadowRootCallBack, ShadowRootsController } from './shadowRootsController'
const DEFAULT_SHADOW_ROOT_CONTROLLER: ShadowRootsController = {
flush: noop,
stop: noop,
addShadowRoot: noop,
removeShadowRoot: noop,
}
describe('startMutationCollection', () => {
let sandbox: HTMLElement
let stopMutationCollection: () => void
let flushMutations: () => void
let addShadowRootSpy: jasmine.Spy<ShadowRootCallBack>
let removeShadowRootSpy: jasmine.Spy<ShadowRootCallBack>
beforeEach(() => {
addShadowRootSpy = jasmine.createSpy<ShadowRootCallBack>()
removeShadowRootSpy = jasmine.createSpy<ShadowRootCallBack>()
})
function startMutationCollection(defaultPrivacyLevel: DefaultPrivacyLevel = DefaultPrivacyLevel.ALLOW) {
const mutationCallbackSpy = jasmine.createSpy<MutationCallBack>()
const mutationController = new MutationController()
;({ stop: stopMutationCollection } = startMutationObserver(mutationController, mutationCallbackSpy, {
defaultPrivacyLevel,
} as RumConfiguration))
;({ stop: stopMutationCollection, flush: flushMutations } = startMutationObserver(
mutationCallbackSpy,
{
defaultPrivacyLevel,
} as RumConfiguration,
{ ...DEFAULT_SHADOW_ROOT_CONTROLLER, addShadowRoot: addShadowRootSpy, removeShadowRoot: removeShadowRootSpy },
document
))
return {
mutationController,
mutationCallbackSpy,

@@ -45,2 +65,3 @@ getLatestMutationPayload: () => mutationCallbackSpy.calls.mostRecent()?.args[0],

{
shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER,
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT,

@@ -70,6 +91,6 @@ elementsScrollPositions: createElementsScrollPositions(),

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection()
const { mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection()
sandbox.appendChild(document.createElement('div'))
mutationController.flush()
flushMutations()

@@ -106,6 +127,6 @@ expect(mutationCallbackSpy).toHaveBeenCalledTimes(1)

// Here, we don't call serializeDocument(), so the sandbox is 'unknown'.
const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
sandbox.appendChild(document.createElement('div'))
mutationController.flush()
flushMutations()

@@ -117,3 +138,3 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

serializeDocumentWithDefaults()
const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()

@@ -124,3 +145,3 @@ sandbox.appendChild(document.createElement('div'))

mutationController.flush()
flushMutations()

@@ -136,7 +157,7 @@ expect(mutationCallbackSpy).toHaveBeenCalledTimes(1)

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
element.setAttribute('foo', 'bar')
sandbox.remove()
mutationController.flush()
flushMutations()

@@ -151,7 +172,7 @@ expect(getLatestMutationPayload().attributes).toEqual([])

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
textNode.data = 'bar'
sandbox.remove()
mutationController.flush()
flushMutations()

@@ -164,7 +185,7 @@ expect(getLatestMutationPayload().texts).toEqual([])

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
sandbox.appendChild(document.createElement('div'))
sandbox.remove()
mutationController.flush()
flushMutations()

@@ -179,7 +200,7 @@ expect(getLatestMutationPayload().adds).toEqual([])

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
element.remove()
sandbox.remove()
mutationController.flush()
flushMutations()

@@ -209,3 +230,3 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()

@@ -216,3 +237,3 @@ element.remove()

element.setAttribute('foo', 'bar')
mutationController.flush()
flushMutations()

@@ -227,3 +248,3 @@ expect(getLatestMutationPayload().attributes).toEqual([])

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()

@@ -234,3 +255,3 @@ textNode.remove()

textNode.data = 'bar'
mutationController.flush()
flushMutations()

@@ -247,3 +268,3 @@ expect(getLatestMutationPayload().texts).toEqual([])

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()

@@ -256,3 +277,3 @@ // Generate a mutation on 'child'

sandbox.appendChild(parent)
mutationController.flush()
flushMutations()

@@ -286,3 +307,3 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()

@@ -295,3 +316,3 @@ const parent = document.createElement('a')

child.remove()
mutationController.flush()
flushMutations()

@@ -314,3 +335,3 @@ const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()

@@ -321,3 +342,3 @@ sandbox.appendChild(element)

mutationController.flush()
flushMutations()

@@ -342,3 +363,3 @@ const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()

@@ -348,3 +369,3 @@ // Moves 'a' after 'b'

mutationController.flush()
flushMutations()

@@ -377,3 +398,3 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()

@@ -383,3 +404,3 @@ container1.appendChild(element)

mutationController.flush()
flushMutations()

@@ -406,3 +427,3 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()

@@ -413,3 +434,3 @@ sandbox.appendChild(document.createElement('a'))

mutationController.flush()
flushMutations()

@@ -442,6 +463,6 @@ const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidator(serializedDocument)

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, getLatestMutationPayload } = startMutationCollection(DefaultPrivacyLevel.MASK)
const { getLatestMutationPayload } = startMutationCollection(DefaultPrivacyLevel.MASK)
sandbox.innerText = 'foo bar'
mutationController.flush()
flushMutations()

@@ -461,2 +482,90 @@ const { validate, expectNewNode, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

})
describe('for shadow DOM', () => {
it('should call addShadowRoot when host is added', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const serializedDocument = serializeDocumentWithDefaults()
const { mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection()
const host = document.createElement('div')
const shadowRoot = host.attachShadow({ mode: 'open' })
shadowRoot.appendChild(document.createElement('span'))
sandbox.appendChild(host)
flushMutations()
expect(mutationCallbackSpy).toHaveBeenCalledTimes(1)
const { validate, expectNewNode, expectInitialNode } = createMutationPayloadValidator(serializedDocument)
const child = expectNewNode({ type: NodeType.Element, tagName: 'span' })
const shadowRootNode = expectNewNode({ type: NodeType.DocumentFragment, isShadowRoot: true }).withChildren(
child
)
const expectedHost = expectNewNode({ type: NodeType.Element, tagName: 'div' }).withChildren(shadowRootNode)
validate(getLatestMutationPayload(), {
adds: [
{
parent: expectInitialNode({ idAttribute: 'sandbox' }),
node: expectedHost,
},
],
})
expect(addShadowRootSpy).toHaveBeenCalledOnceWith(shadowRoot)
expect(removeShadowRootSpy).not.toHaveBeenCalled()
})
it('should call removeShadowRoot when host is removed', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const host = document.createElement('div')
host.id = 'host'
const shadowRoot = host.attachShadow({ mode: 'open' })
shadowRoot.appendChild(document.createElement('span'))
sandbox.appendChild(host)
const serializedDocument = serializeDocumentWithDefaults()
const { mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection()
host.remove()
flushMutations()
expect(mutationCallbackSpy).toHaveBeenCalledTimes(1)
const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)
validate(getLatestMutationPayload(), {
removes: [
{
parent: expectInitialNode({ idAttribute: 'sandbox' }),
node: expectInitialNode({ idAttribute: 'host' }),
},
],
})
expect(addShadowRootSpy).not.toHaveBeenCalled()
expect(removeShadowRootSpy).toHaveBeenCalledOnceWith(shadowRoot)
})
it('should call removeShadowRoot when parent of host is removed', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const parent = document.createElement('div')
parent.id = 'parent'
const host = document.createElement('div')
host.id = 'host'
parent.appendChild(host)
const shadowRoot = host.attachShadow({ mode: 'open' })
shadowRoot.appendChild(document.createElement('span'))
sandbox.appendChild(parent)
const serializedDocument = serializeDocumentWithDefaults()
const { mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection()
parent.remove()
flushMutations()
expect(mutationCallbackSpy).toHaveBeenCalledTimes(1)
const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)
validate(getLatestMutationPayload(), {
removes: [
{
parent: expectInitialNode({ idAttribute: 'sandbox' }),
node: expectInitialNode({ idAttribute: 'parent' }),
},
],
})
expect(addShadowRootSpy).not.toHaveBeenCalled()
expect(removeShadowRootSpy).toHaveBeenCalledOnceWith(shadowRoot)
})
})
})

@@ -474,6 +583,6 @@

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection()
const { mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection()
textNode.data = 'bar'
mutationController.flush()
flushMutations()

@@ -495,7 +604,7 @@ expect(mutationCallbackSpy).toHaveBeenCalledTimes(1)

serializeDocumentWithDefaults()
const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
textNode.data = 'bar'
textNode.data = 'foo'
mutationController.flush()
flushMutations()

@@ -507,6 +616,6 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, getLatestMutationPayload } = startMutationCollection(DefaultPrivacyLevel.MASK)
const { getLatestMutationPayload } = startMutationCollection(DefaultPrivacyLevel.MASK)
textNode.data = 'foo bar'
mutationController.flush()
flushMutations()

@@ -534,8 +643,6 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection(
DefaultPrivacyLevel.MASK
)
const { mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection(DefaultPrivacyLevel.MASK)
div.firstChild!.textContent = 'bazz 7'
mutationController.flush()
flushMutations()

@@ -559,6 +666,6 @@ expect(mutationCallbackSpy).toHaveBeenCalledTimes(1)

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection()
const { mutationCallbackSpy, getLatestMutationPayload } = startMutationCollection()
sandbox.setAttribute('foo', 'bar')
mutationController.flush()
flushMutations()

@@ -580,6 +687,6 @@ expect(mutationCallbackSpy).toHaveBeenCalledTimes(1)

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
sandbox.setAttribute('foo', '')
mutationController.flush()
flushMutations()

@@ -600,6 +707,6 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
sandbox.removeAttribute('foo')
mutationController.flush()
flushMutations()

@@ -620,7 +727,7 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

serializeDocumentWithDefaults()
const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
sandbox.setAttribute('foo', 'biz')
sandbox.setAttribute('foo', 'bar')
mutationController.flush()
flushMutations()

@@ -632,7 +739,7 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
sandbox.setAttribute('foo1', 'biz')
sandbox.setAttribute('foo2', 'bar')
mutationController.flush()
flushMutations()

@@ -652,6 +759,6 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const serializedDocument = serializeDocumentWithDefaults()
const { mutationController, getLatestMutationPayload } = startMutationCollection(DefaultPrivacyLevel.MASK)
const { getLatestMutationPayload } = startMutationCollection(DefaultPrivacyLevel.MASK)
sandbox.setAttribute('data-foo', 'biz')
mutationController.flush()
flushMutations()

@@ -681,7 +788,7 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
sandbox.insertBefore(document.createElement('a'), ignoredElement)
mutationController.flush()
flushMutations()

@@ -704,7 +811,7 @@ const { validate, expectInitialNode, expectNewNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
sandbox.appendChild(ignoredElement)
mutationController.flush()
flushMutations()

@@ -717,7 +824,7 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
ignoredElement.setAttribute('foo', 'bar')
mutationController.flush()
flushMutations()

@@ -730,7 +837,7 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
ignoredElement.appendChild(document.createTextNode('function foo() {}'))
mutationController.flush()
flushMutations()

@@ -746,7 +853,7 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
textNode.data = 'function bar() {}'
mutationController.flush()
flushMutations()

@@ -761,7 +868,7 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
ignoredElement.appendChild(textNode)
mutationController.flush()
flushMutations()

@@ -789,6 +896,6 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
sandbox.appendChild(script)
mutationController.flush()
flushMutations()

@@ -811,7 +918,7 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
hiddenElement.setAttribute('foo', 'bar')
mutationController.flush()
flushMutations()

@@ -825,7 +932,7 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
hiddenElement.appendChild(document.createTextNode('function foo() {}'))
mutationController.flush()
flushMutations()

@@ -841,7 +948,7 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const { mutationController, mutationCallbackSpy } = startMutationCollection()
const { mutationCallbackSpy } = startMutationCollection()
textNode.data = 'function bar() {}'
mutationController.flush()
flushMutations()

@@ -856,7 +963,7 @@ expect(mutationCallbackSpy).not.toHaveBeenCalled()

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
hiddenElement.appendChild(textNode)
mutationController.flush()
flushMutations()

@@ -947,6 +1054,6 @@ const { validate, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, getLatestMutationPayload } = startMutationCollection()
const { getLatestMutationPayload } = startMutationCollection()
sandbox.appendChild(input)
mutationController.flush()
flushMutations()

@@ -979,6 +1086,6 @@ const { validate, expectNewNode, expectInitialNode } = createMutationPayloadValidator(serializedDocument)

const { mutationController, getLatestMutationPayload, mutationCallbackSpy } = startMutationCollection()
const { getLatestMutationPayload, mutationCallbackSpy } = startMutationCollection()
input.setAttribute('value', 'bar')
mutationController.flush()
flushMutations()

@@ -985,0 +1092,0 @@ if (expectedAttributesMutation) {

@@ -1,4 +0,9 @@

import { monitor, noop } from '@datadog/browser-core'
import { isExperimentalFeatureEnabled, monitor, noop } from '@datadog/browser-core'
import type { RumConfiguration } from '@datadog/browser-rum-core'
import { getMutationObserverConstructor } from '@datadog/browser-rum-core'
import {
getChildNodes,
isNodeShadowHost,
getMutationObserverConstructor,
getParentNode,
} from '@datadog/browser-rum-core'
import { NodePrivacyLevel } from '../../constants'

@@ -18,2 +23,3 @@ import type { AddedNodeMutation, AttributeMutation, RemovedNodeMutation, TextMutation } from '../../types'

import type { MutationCallBack } from './observers'
import type { ShadowRootCallBack, ShadowRootsController } from './shadowRootsController'

@@ -52,12 +58,20 @@ type WithSerializedTarget<T> = T & { target: NodeWithSerializedNode }

export function startMutationObserver(
controller: MutationController,
mutationCallback: MutationCallBack,
configuration: RumConfiguration
configuration: RumConfiguration,
shadowRootsController: ShadowRootsController,
target: Node
) {
const MutationObserver = getMutationObserverConstructor()
if (!MutationObserver) {
return { stop: noop }
return { stop: noop, flush: noop }
}
const mutationBatch = createMutationBatch((mutations) => {
processMutations(mutations.concat(observer.takeRecords() as RumMutationRecord[]), mutationCallback, configuration)
processMutations(
mutations.concat(observer.takeRecords() as RumMutationRecord[]),
mutationCallback,
configuration,
shadowRootsController,
target
)
})

@@ -67,3 +81,3 @@

observer.observe(document, {
observer.observe(target, {
attributeOldValue: true,

@@ -76,3 +90,2 @@ attributes: true,

})
controller.onFlush(mutationBatch.flush)

@@ -84,25 +97,23 @@ return {

},
flush: () => {
mutationBatch.flush()
},
}
}
/**
* Controls how mutations are processed, allowing to flush pending mutations.
*/
export class MutationController {
private flushListener?: () => void
public flush() {
this.flushListener?.()
}
public onFlush(listener: () => void) {
this.flushListener = listener
}
}
function processMutations(
mutations: RumMutationRecord[],
mutationCallback: MutationCallBack,
configuration: RumConfiguration
configuration: RumConfiguration,
shadowRootsController: ShadowRootsController,
target: Node
) {
mutations
.filter((mutation): mutation is RumChildListMutationRecord => mutation.type === 'childList')
.forEach((mutation) => {
mutation.removedNodes.forEach((removedNode) => {
traverseRemovedShadowDom(removedNode, shadowRootsController.removeShadowRoot)
})
})
// Discard any mutation with a 'target' node that:

@@ -114,3 +125,3 @@ // * isn't injected in the current document or isn't known/serialized yet: those nodes are likely

(mutation): mutation is WithSerializedTarget<RumMutationRecord> =>
document.contains(mutation.target) &&
target.contains(mutation.target) &&
nodeAndAncestorsHaveSerializedNode(mutation.target) &&

@@ -124,3 +135,4 @@ getNodePrivacyLevel(mutation.target, configuration.defaultPrivacyLevel) !== NodePrivacyLevel.HIDDEN

),
configuration
configuration,
shadowRootsController
)

@@ -158,3 +170,4 @@

mutations: Array<WithSerializedTarget<RumChildListMutationRecord>>,
configuration: RumConfiguration
configuration: RumConfiguration,
shadowRootsController: ShadowRootsController
) {

@@ -217,3 +230,3 @@ // First, we iterate over mutations to collect:

parentNodePrivacyLevel,
serializationContext: { status: SerializationContextStatus.MUTATION },
serializationContext: { status: SerializationContextStatus.MUTATION, shadowRootsController },
configuration,

@@ -225,5 +238,6 @@ })

const parentNode = getParentNode(node)!
addedNodeMutations.push({
nextId: getNextSibling(node),
parentId: getSerializedNodeId(node.parentNode!)!,
parentId: getSerializedNodeId(parentNode)!,
node: serializedNode,

@@ -285,3 +299,6 @@ })

const parentNodePrivacyLevel = getNodePrivacyLevel(mutation.target.parentNode!, configuration.defaultPrivacyLevel)
const parentNodePrivacyLevel = getNodePrivacyLevel(
getParentNode(mutation.target)!,
configuration.defaultPrivacyLevel
)
if (parentNodePrivacyLevel === NodePrivacyLevel.HIDDEN || parentNodePrivacyLevel === NodePrivacyLevel.IGNORE) {

@@ -378,1 +395,10 @@ continue

}
function traverseRemovedShadowDom(removedNode: Node, shadowDomRemovedCallback: ShadowRootCallBack) {
if (!isExperimentalFeatureEnabled('record_shadow_dom')) {
return
}
if (isNodeShadowHost(removedNode)) {
shadowDomRemovedCallback(removedNode.shadowRoot)
}
getChildNodes(removedNode).forEach((child) => traverseRemovedShadowDom(child, shadowDomRemovedCallback))
}

@@ -1,2 +0,9 @@

import { DefaultPrivacyLevel, isIE, relativeNow, timeStampNow } from '@datadog/browser-core'
import {
DefaultPrivacyLevel,
isIE,
noop,
relativeNow,
timeStampNow,
updateExperimentalFeatures,
} from '@datadog/browser-core'
import type { RawRumActionEvent, RumConfiguration } from '@datadog/browser-rum-core'

@@ -12,3 +19,11 @@ import { ActionType, LifeCycle, LifeCycleEventType, RumEventType, FrustrationType } from '@datadog/browser-rum-core'

import { createElementsScrollPositions } from './elementsScrollPositions'
import type { ShadowRootsController } from './shadowRootsController'
const DEFAULT_SHADOW_ROOT_CONTROLLER: ShadowRootsController = {
flush: noop,
stop: noop,
addShadowRoot: noop,
removeShadowRoot: noop,
}
const DEFAULT_CONFIGURATION = { defaultPrivacyLevel: NodePrivacyLevel.ALLOW } as RumConfiguration

@@ -34,2 +49,3 @@

serializeDocument(document, DEFAULT_CONFIGURATION, {
shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER,
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT,

@@ -55,2 +71,14 @@ elementsScrollPositions: createElementsScrollPositions(),

// cannot trigger a event in a Shadow DOM because event with `isTrusted:false` do not cross the root
it('collects input values when an "input" event is composed', () => {
updateExperimentalFeatures(['record_shadow_dom'])
stopInputObserver = initInputObserver(inputCallbackSpy, DefaultPrivacyLevel.ALLOW)
dispatchInputEventWithInShadowDom('foo')
expect(inputCallbackSpy).toHaveBeenCalledOnceWith({
text: 'foo',
id: jasmine.any(Number) as unknown as number,
})
})
it('masks input values according to the element privacy level', () => {

@@ -86,2 +114,11 @@ stopInputObserver = initInputObserver(inputCallbackSpy, DefaultPrivacyLevel.ALLOW)

}
function dispatchInputEventWithInShadowDom(newValue: string) {
input.value = newValue
const host = document.createElement('div')
host.attachShadow({ mode: 'open' })
const event = createNewEvent('input', { target: host, composed: true })
event.composedPath = () => [input, host, sandbox, document.body]
input.dispatchEvent(event)
}
})

@@ -181,2 +218,3 @@

serializeDocument(document, DEFAULT_CONFIGURATION, {
shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER,
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT,

@@ -183,0 +221,0 @@ elementsScrollPositions: createElementsScrollPositions(),

@@ -12,5 +12,12 @@ import type { DefaultPrivacyLevel } from '@datadog/browser-core'

noop,
isExperimentalFeatureEnabled,
} from '@datadog/browser-core'
import type { LifeCycle, RumConfiguration } from '@datadog/browser-rum-core'
import { initViewportObservable, ActionType, RumEventType, LifeCycleEventType } from '@datadog/browser-rum-core'
import {
isNodeShadowHost,
initViewportObservable,
ActionType,
RumEventType,
LifeCycleEventType,
} from '@datadog/browser-rum-core'
import { NodePrivacyLevel } from '../../constants'

@@ -36,6 +43,6 @@ import type {

import { assembleIncrementalSnapshot, forEach, getPathToNestedCSSRule, isTouchEvent } from './utils'
import type { MutationController } from './mutationObserver'
import { startMutationObserver } from './mutationObserver'
import { getVisualViewport, getScrollX, getScrollY, convertMouseEventToLayoutCoordinates } from './viewports'
import type { ElementsScrollPositions } from './elementsScrollPositions'
import type { ShadowRootsController } from './shadowRootsController'

@@ -88,3 +95,2 @@ const MOUSE_MOVE_OBSERVER_THRESHOLD = 50

configuration: RumConfiguration
mutationController: MutationController
elementsScrollPositions: ElementsScrollPositions

@@ -102,6 +108,7 @@ mutationCb: MutationCallBack

frustrationCb: FrustrationCallback
shadowRootsController: ShadowRootsController
}
export function initObservers(o: ObserverParam): ListenerHandler {
const mutationHandler = initMutationObserver(o.mutationController, o.mutationCb, o.configuration)
export function initObservers(o: ObserverParam): { stop: ListenerHandler; flush: ListenerHandler } {
const mutationHandler = initMutationObserver(o.mutationCb, o.configuration, o.shadowRootsController)
const mousemoveHandler = initMoveObserver(o.mousemoveCb)

@@ -124,23 +131,28 @@ const mouseInteractionHandler = initMouseInteractionObserver(

return () => {
mutationHandler()
mousemoveHandler()
mouseInteractionHandler()
scrollHandler()
viewportResizeHandler()
inputHandler()
mediaInteractionHandler()
styleSheetObserver()
focusHandler()
visualViewportResizeHandler()
frustrationHandler()
return {
flush: () => {
mutationHandler.flush()
},
stop: () => {
mutationHandler.stop()
mousemoveHandler()
mouseInteractionHandler()
scrollHandler()
viewportResizeHandler()
inputHandler()
mediaInteractionHandler()
styleSheetObserver()
focusHandler()
visualViewportResizeHandler()
frustrationHandler()
},
}
}
function initMutationObserver(
mutationController: MutationController,
export function initMutationObserver(
cb: MutationCallBack,
configuration: RumConfiguration
configuration: RumConfiguration,
shadowRootsController: ShadowRootsController
) {
return startMutationObserver(mutationController, cb, configuration).stop
return startMutationObserver(cb, configuration, shadowRootsController, document)
}

@@ -151,3 +163,3 @@

monitor((event: MouseEvent | TouchEvent) => {
const target = event.target as Node
const target = getEventTarget(event)
if (hasSerializedNode(target)) {

@@ -197,3 +209,3 @@ const { clientX, clientY } = isTouchEvent(event) ? event.changedTouches[0] : event

const handler = (event: MouseEvent | TouchEvent) => {
const target = event.target as Node
const target = getEventTarget(event)
if (getNodePrivacyLevel(target, defaultPrivacyLevel) === NodePrivacyLevel.HIDDEN || !hasSerializedNode(target)) {

@@ -234,3 +246,3 @@ return

monitor((event: UIEvent) => {
const target = event.target as HTMLElement | Document
const target = getEventTarget(event) as HTMLElement | Document
if (

@@ -270,3 +282,11 @@ !target ||

export function initInputObserver(cb: InputCallback, defaultPrivacyLevel: DefaultPrivacyLevel): ListenerHandler {
type InputObserverOptions = {
domEvents?: Array<DOM_EVENT.INPUT | DOM_EVENT.CHANGE>
target?: Node
}
export function initInputObserver(
cb: InputCallback,
defaultPrivacyLevel: DefaultPrivacyLevel,
{ domEvents = [DOM_EVENT.INPUT, DOM_EVENT.CHANGE], target = document }: InputObserverOptions = {}
): ListenerHandler {
const lastInputStateMap: WeakMap<Node, InputState> = new WeakMap()

@@ -337,11 +357,12 @@

const { stop: stopEventListeners } = addEventListeners(
document,
[DOM_EVENT.INPUT, DOM_EVENT.CHANGE],
target,
domEvents,
(event) => {
const target = getEventTarget(event)
if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
event.target instanceof HTMLSelectElement
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement
) {
onElementChange(event.target)
onElementChange(target)
}

@@ -431,3 +452,3 @@ },

const handler = (event: Event) => {
const target = event.target as Node
const target = getEventTarget(event)
if (

@@ -503,1 +524,12 @@ !target ||

}
function getEventTarget(event: Event): Node {
if (
event.composed === true &&
isNodeShadowHost(event.target as Node) &&
isExperimentalFeatureEnabled('record_shadow_dom')
) {
return event.composedPath()[0] as Node
}
return event.target as Node
}

@@ -74,2 +74,11 @@ import { isIE } from '@datadog/browser-core'

})
it('returns an ancestor privacy mode if the element has none and cross shadow DOM', () => {
const ancestor = document.createElement('div')
ancestor.attachShadow({ mode: 'open' })
const node = document.createElement('div')
ancestor.setAttribute(PRIVACY_ATTR_NAME, PRIVACY_ATTR_VALUE_MASK)
ancestor.shadowRoot!.appendChild(node)
expect(getNodePrivacyLevel(node, NodePrivacyLevel.ALLOW)).toBe(NodePrivacyLevel.MASK)
})
})

@@ -76,0 +85,0 @@ })

@@ -0,1 +1,2 @@

import { isElementNode, getParentNode, isTextNode } from '@datadog/browser-rum-core'
import {

@@ -27,5 +28,4 @@ NodePrivacyLevel,

export function getNodePrivacyLevel(node: Node, defaultPrivacyLevel: NodePrivacyLevel): NodePrivacyLevel {
const parentNodePrivacyLevel = node.parentNode
? getNodePrivacyLevel(node.parentNode, defaultPrivacyLevel)
: defaultPrivacyLevel
const parentNode = getParentNode(node)
const parentNodePrivacyLevel = parentNode ? getNodePrivacyLevel(parentNode, defaultPrivacyLevel) : defaultPrivacyLevel
const selfNodePrivacyLevel = getNodeSelfPrivacyLevel(node)

@@ -65,3 +65,3 @@ return reducePrivacyLevel(selfNodePrivacyLevel, parentNodePrivacyLevel)

// Only Element types can have a privacy level set
if (!isElement(node)) {
if (!isElementNode(node)) {
return

@@ -139,10 +139,2 @@ }

function isElement(node: Node): node is Element {
return node.nodeType === node.ELEMENT_NODE
}
function isTextNode(node: Node): node is Text {
return node.nodeType === node.TEXT_NODE
}
function isFormElement(node: Node | null): boolean {

@@ -149,0 +141,0 @@ if (!node || node.nodeType !== node.ELEMENT_NODE) {

@@ -1,2 +0,8 @@

import { DefaultPrivacyLevel, isIE } from '@datadog/browser-core'
import {
DefaultPrivacyLevel,
findLast,
isIE,
resetExperimentalFeatures,
updateExperimentalFeatures,
} from '@datadog/browser-core'
import type { RumConfiguration } from '@datadog/browser-rum-core'

@@ -6,5 +12,12 @@ import { LifeCycle } from '@datadog/browser-rum-core'

import { createNewEvent } from '../../../../core/test/specHelper'
import { collectAsyncCalls, recordsPerFullSnapshot } from '../../../test/utils'
import type { BrowserIncrementalSnapshotRecord, BrowserRecord, FocusRecord } from '../../types'
import { RecordType, IncrementalSource } from '../../types'
import { collectAsyncCalls, findFullSnapshot, findNode, recordsPerFullSnapshot } from '../../../test/utils'
import type {
BrowserIncrementalSnapshotRecord,
BrowserMutationData,
BrowserRecord,
DocumentFragmentNode,
ElementNode,
FocusRecord,
} from '../../types'
import { NodeType, RecordType, IncrementalSource } from '../../types'
import type { RecordAPI } from './record'

@@ -200,2 +213,198 @@ import { record } from './record'

describe('Shadow dom', () => {
let sandbox: HTMLElement
beforeEach(() => {
sandbox = document.createElement('div')
sandbox.id = 'sandbox'
document.body.appendChild(sandbox)
})
afterEach(() => {
sandbox.remove()
resetExperimentalFeatures()
})
it('should record a simple mutation inside a shadow root', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const div = document.createElement('div')
div.className = 'toto'
createShadow([div])
startRecording()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
div.className = 'titi'
recordApi.flushMutations()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
const innerMutationData = getLastIncrementalSnapshotData<BrowserMutationData>(
getEmittedRecords(),
IncrementalSource.Mutation
)
expect(innerMutationData.attributes[0].attributes.class).toBe('titi')
})
it('should record a direct removal inside a shadow root', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const span = document.createElement('span')
createShadow([span])
startRecording()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
span.remove()
recordApi.flushMutations()
const fs = findFullSnapshot({ records: getEmittedRecords() })!
const shadowRootNode = findNode(
fs.data.node,
(node) => node.type === NodeType.DocumentFragment && node.isShadowRoot
)!
expect(shadowRootNode).toBeTruthy()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
const innerMutationData = getLastIncrementalSnapshotData<BrowserMutationData>(
getEmittedRecords(),
IncrementalSource.Mutation
)
expect(innerMutationData.removes.length).toBe(1)
expect(innerMutationData.removes[0].parentId).toBe(shadowRootNode.id)
})
it('should record a direct addition inside a shadow root', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const span = document.createElement('span')
const shadowRoot = createShadow([span])
startRecording()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
shadowRoot.appendChild(document.createElement('span'))
recordApi.flushMutations()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
const fs = findFullSnapshot({ records: getEmittedRecords() })!
const shadowRootNode = findNode(
fs.data.node,
(node) => node.type === NodeType.DocumentFragment && node.isShadowRoot
)!
expect(shadowRootNode).toBeTruthy()
const innerMutationData = getLastIncrementalSnapshotData<BrowserMutationData>(
getEmittedRecords(),
IncrementalSource.Mutation
)
expect(innerMutationData.adds.length).toBe(1)
expect(innerMutationData.adds[0].node.type).toBe(2)
expect(innerMutationData.adds[0].parentId).toBe(shadowRootNode.id)
const addedNode = innerMutationData.adds[0].node as ElementNode
expect(addedNode.tagName).toBe('span')
})
it('should record mutation inside a shadow root added after the FS', () => {
updateExperimentalFeatures(['record_shadow_dom'])
startRecording()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
// shadow DOM mutation
const span = document.createElement('span')
span.className = 'toto'
createShadow([span])
recordApi.flushMutations()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
const hostMutationData = getLastIncrementalSnapshotData<BrowserMutationData>(
getEmittedRecords(),
IncrementalSource.Mutation
)
expect(hostMutationData.adds.length).toBe(1)
const hostNode = hostMutationData.adds[0].node as ElementNode
const shadowRoot = hostNode.childNodes[0] as DocumentFragmentNode
expect(shadowRoot.type).toBe(NodeType.DocumentFragment)
expect(shadowRoot.isShadowRoot).toBe(true)
// inner mutation
span.className = 'titi'
recordApi.flushMutations()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 2)
const innerMutationData = getLastIncrementalSnapshotData<BrowserMutationData>(
getEmittedRecords(),
IncrementalSource.Mutation
)
expect(innerMutationData.attributes.length).toBe(1)
expect(innerMutationData.attributes[0].attributes.class).toBe('titi')
})
it('should record the change event inside a shadow root', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const radio = document.createElement('input')
radio.setAttribute('type', 'radio')
createShadow([radio])
startRecording()
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
// inner mutation
radio.checked = true
radio.dispatchEvent(createNewEvent('change', { target: radio, composed: false }))
recordApi.flushMutations()
const innerMutationData = getLastIncrementalSnapshotData<BrowserMutationData & { isChecked: boolean }>(
getEmittedRecords(),
IncrementalSource.Input
)
expect(innerMutationData.isChecked).toBe(true)
})
it('should clean the state once the shadow dom is removed to avoid memory leak', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const div = document.createElement('div')
div.className = 'toto'
const shadowRoot = createShadow([div])
startRecording()
spyOn(recordApi.shadowRootsController, 'removeShadowRoot')
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(0)
shadowRoot.host.remove()
recordApi.flushMutations()
expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(1)
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
const mutationData = getLastIncrementalSnapshotData<BrowserMutationData>(
getEmittedRecords(),
IncrementalSource.Mutation
)
expect(mutationData.removes.length).toBe(1)
})
it('should clean the state when both the parent and the shadow host is removed to avoid memory leak', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const grandParent = document.createElement('div')
const parent = document.createElement('div')
grandParent.appendChild(parent)
const child = document.createElement('div')
child.className = 'toto'
createShadow([child], parent)
sandbox.appendChild(grandParent)
startRecording()
spyOn(recordApi.shadowRootsController, 'removeShadowRoot')
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot())
expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(0)
parent.remove()
grandParent.remove()
recordApi.flushMutations()
expect(recordApi.shadowRootsController.removeShadowRoot).toHaveBeenCalledTimes(1)
expect(getEmittedRecords().length).toBe(recordsPerFullSnapshot() + 1)
const mutationData = getLastIncrementalSnapshotData<BrowserMutationData>(
getEmittedRecords(),
IncrementalSource.Mutation
)
expect(mutationData.removes.length).toBe(1)
})
function createShadow(children: Element[], parent = sandbox) {
const host = document.createElement('div')
host.setAttribute('id', 'host')
const shadowRoot = host.attachShadow({ mode: 'open' })
children.forEach((child) => shadowRoot.appendChild(child))
parent.append(host)
return shadowRoot
}
})
function startRecording() {

@@ -220,1 +429,14 @@ recordApi = record({

}
export function getLastIncrementalSnapshotData<T extends BrowserIncrementalSnapshotRecord['data']>(
records: BrowserRecord[],
source: IncrementalSource
): T {
const record = findLast(
records,
(record): record is BrowserIncrementalSnapshotRecord & { data: T } =>
record.type === RecordType.IncrementalSnapshot && record.data.source === source
)
expect(record).toBeTruthy(`Could not find IncrementalSnapshot/${source} in ${records.length} records`)
return record!.data
}

@@ -7,2 +7,3 @@ import type { TimeStamp } from '@datadog/browser-core'

BrowserMutationData,
BrowserMutationPayload,
BrowserRecord,

@@ -19,7 +20,9 @@ InputData,

import { initObservers } from './observers'
import type { InputCallback } from './observers'
import { MutationController } from './mutationObserver'
import { getVisualViewport, getScrollX, getScrollY } from './viewports'
import { assembleIncrementalSnapshot } from './utils'
import { createElementsScrollPositions } from './elementsScrollPositions'
import type { ShadowRootsController } from './shadowRootsController'
import { initShadowRootsController } from './shadowRootsController'

@@ -36,2 +39,3 @@ export interface RecordOptions {

flushMutations: () => void
shadowRootsController: ShadowRootsController
}

@@ -46,10 +50,19 @@

const mutationController = new MutationController()
const elementsScrollPositions = createElementsScrollPositions()
const mutationCb = (mutation: BrowserMutationPayload) => {
emit(assembleIncrementalSnapshot<BrowserMutationData>(IncrementalSource.Mutation, mutation))
}
const inputCb: InputCallback = (s) => emit(assembleIncrementalSnapshot<InputData>(IncrementalSource.Input, s))
const shadowRootsController = initShadowRootsController(options.configuration, { mutationCb, inputCb })
const takeFullSnapshot = (
timestamp = timeStampNow(),
serializationContext = { status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT, elementsScrollPositions }
serializationContext = {
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT,
elementsScrollPositions,
shadowRootsController,
}
) => {
mutationController.flush() // process any pending mutation before taking a full snapshot
const { width, height } = getViewportDimension()

@@ -97,8 +110,7 @@ emit({

const stopObservers = initObservers({
const { stop: stopObservers, flush: flushMutationsFromObservers } = initObservers({
lifeCycle: options.lifeCycle,
configuration: options.configuration,
mutationController,
elementsScrollPositions,
inputCb: (v) => emit(assembleIncrementalSnapshot<InputData>(IncrementalSource.Input, v)),
inputCb,
mediaInteractionCb: (p) =>

@@ -108,3 +120,3 @@ emit(assembleIncrementalSnapshot<MediaInteractionData>(IncrementalSource.MediaInteraction, p)),

mousemoveCb: (positions, source) => emit(assembleIncrementalSnapshot<MousemoveData>(source, { positions })),
mutationCb: (m) => emit(assembleIncrementalSnapshot<BrowserMutationData>(IncrementalSource.Mutation, m)),
mutationCb,
scrollCb: (p) => emit(assembleIncrementalSnapshot<ScrollData>(IncrementalSource.Scroll, p)),

@@ -128,13 +140,26 @@ styleSheetCb: (r) => emit(assembleIncrementalSnapshot<StyleSheetRuleData>(IncrementalSource.StyleSheetRule, r)),

},
shadowRootsController,
})
function flushMutations() {
shadowRootsController.flush()
flushMutationsFromObservers()
}
return {
stop: stopObservers,
takeSubsequentFullSnapshot: (timestamp) =>
stop: () => {
shadowRootsController.stop()
stopObservers()
},
takeSubsequentFullSnapshot: (timestamp) => {
flushMutations()
takeFullSnapshot(timestamp, {
shadowRootsController,
status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT,
elementsScrollPositions,
}),
flushMutations: () => mutationController.flush(),
})
},
flushMutations,
shadowRootsController,
}
}
import { buildUrl } from '@datadog/browser-core'
import { getParentNode, isNodeShadowRoot } from '@datadog/browser-rum-core'
import type { NodePrivacyLevel } from '../../constants'

@@ -17,6 +18,6 @@ import { CENSORED_STRING_MARK } from '../../constants'

while (current) {
if (!hasSerializedNode(current)) {
if (!hasSerializedNode(current) && !isNodeShadowRoot(current)) {
return false
}
current = current.parentNode
current = getParentNode(current)
}

@@ -23,0 +24,0 @@ return true

@@ -1,2 +0,2 @@

import { isIE } from '@datadog/browser-core'
import { isIE, noop, resetExperimentalFeatures, updateExperimentalFeatures } from '@datadog/browser-core'
import type { RumConfiguration } from '@datadog/browser-rum-core'

@@ -23,3 +23,3 @@ import { STABLE_ATTRIBUTES, DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE } from '@datadog/browser-rum-core'

import { hasSerializedNode } from './serializationUtils'
import type { SerializeOptions } from './serialize'
import type { SerializationContext, SerializeOptions } from './serialize'
import {

@@ -36,6 +36,15 @@ serializeDocument,

import { createElementsScrollPositions } from './elementsScrollPositions'
import type { ShadowRootCallBack, ShadowRootsController } from './shadowRootsController'
const DEFAULT_CONFIGURATION = {} as RumConfiguration
const DEFAULT_SERIALIZATION_CONTEXT = {
const DEFAULT_SHADOW_ROOT_CONTROLLER: ShadowRootsController = {
flush: noop,
stop: noop,
addShadowRoot: noop,
removeShadowRoot: noop,
}
const DEFAULT_SERIALIZATION_CONTEXT: SerializationContext = {
shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER,
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT,

@@ -53,4 +62,9 @@ elementsScrollPositions: createElementsScrollPositions(),

let sandbox: HTMLElement
let addShadowRootSpy: jasmine.Spy<ShadowRootCallBack>
beforeEach(() => {
addShadowRootSpy = jasmine.createSpy<ShadowRootCallBack>()
})
beforeEach(() => {
if (isIE()) {

@@ -65,2 +79,3 @@ pending('IE not supported')

afterEach(() => {
resetExperimentalFeatures()
sandbox.remove()

@@ -156,2 +171,3 @@ })

serializationContext: {
shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER,
status: SerializationContextStatus.INITIAL_FULL_SNAPSHOT,

@@ -177,2 +193,3 @@ elementsScrollPositions,

serializationContext: {
shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER,
status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT,

@@ -196,2 +213,3 @@ elementsScrollPositions,

serializationContext: {
shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER,
status: SerializationContextStatus.SUBSEQUENT_FULL_SNAPSHOT,

@@ -218,2 +236,3 @@ elementsScrollPositions,

serializationContext: {
shadowRootsController: DEFAULT_SHADOW_ROOT_CONTROLLER,
status: SerializationContextStatus.MUTATION,

@@ -413,2 +432,92 @@ },

})
it('serializes a shadow host', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const div = document.createElement('div')
div.attachShadow({ mode: 'open' })
expect(serializeNodeWithId(div, DEFAULT_OPTIONS)).toEqual({
type: NodeType.Element,
tagName: 'div',
attributes: {},
isSVG: undefined,
childNodes: [
{
type: NodeType.DocumentFragment,
isShadowRoot: true,
childNodes: [],
id: jasmine.any(Number) as unknown as number,
},
],
id: jasmine.any(Number) as unknown as number,
})
})
it('serializes a shadow host with children', () => {
updateExperimentalFeatures(['record_shadow_dom'])
const div = document.createElement('div')
div.attachShadow({ mode: 'open' })
div.shadowRoot!.appendChild(document.createElement('hr'))
const options: SerializeOptions = {
...DEFAULT_OPTIONS,
serializationContext: {
...DEFAULT_SERIALIZATION_CONTEXT,
shadowRootsController: {
...DEFAULT_SHADOW_ROOT_CONTROLLER,
addShadowRoot: addShadowRootSpy,
},
},
}
expect(serializeNodeWithId(div, options)).toEqual({
type: NodeType.Element,
tagName: 'div',
attributes: {},
isSVG: undefined,
childNodes: [
{
type: NodeType.DocumentFragment,
isShadowRoot: true,
childNodes: [
{
type: NodeType.Element,
tagName: 'hr',
attributes: {},
isSVG: undefined,
childNodes: [],
id: jasmine.any(Number) as unknown as number,
},
],
id: jasmine.any(Number) as unknown as number,
},
],
id: jasmine.any(Number) as unknown as number,
})
expect(addShadowRootSpy).toHaveBeenCalledWith(div.shadowRoot!)
})
it('does not serialize shadow host children when the experimental flag is missing', () => {
const div = document.createElement('div')
div.attachShadow({ mode: 'open' })
div.shadowRoot!.appendChild(document.createElement('hr'))
const options: SerializeOptions = {
...DEFAULT_OPTIONS,
serializationContext: {
...DEFAULT_SERIALIZATION_CONTEXT,
shadowRootsController: {
...DEFAULT_SHADOW_ROOT_CONTROLLER,
addShadowRoot: addShadowRootSpy,
},
},
}
expect(serializeNodeWithId(div, options)).toEqual({
type: NodeType.Element,
tagName: 'div',
attributes: {},
isSVG: undefined,
childNodes: [],
id: jasmine.any(Number) as unknown as number,
})
expect(addShadowRootSpy).not.toHaveBeenCalled()
})
})

@@ -415,0 +524,0 @@

@@ -1,4 +0,4 @@

import { assign, startsWith } from '@datadog/browser-core'
import { assign, isExperimentalFeatureEnabled, startsWith } from '@datadog/browser-core'
import type { RumConfiguration } from '@datadog/browser-rum-core'
import { STABLE_ATTRIBUTES } from '@datadog/browser-rum-core'
import { isNodeShadowHost, isNodeShadowRoot, STABLE_ATTRIBUTES } from '@datadog/browser-rum-core'
import {

@@ -19,2 +19,3 @@ NodePrivacyLevel,

CDataNode,
DocumentFragmentNode,
} from '../../types'

@@ -37,2 +38,3 @@ import { NodeType } from '../../types'

import type { ElementsScrollPositions } from './elementsScrollPositions'
import type { ShadowRootsController } from './shadowRootsController'

@@ -56,2 +58,3 @@ // Those values are the only one that can be used when inheriting privacy levels from parent to

status: SerializationContextStatus.MUTATION
shadowRootsController: ShadowRootsController
}

@@ -61,2 +64,3 @@ | {

elementsScrollPositions: ElementsScrollPositions
shadowRootsController: ShadowRootsController
}

@@ -66,2 +70,3 @@ | {

elementsScrollPositions: ElementsScrollPositions
shadowRootsController: ShadowRootsController
}

@@ -111,2 +116,4 @@

return serializeDocumentNode(node as Document, options)
case node.DOCUMENT_FRAGMENT_NODE:
return serializeDocumentFragmentNode(node as DocumentFragment, options)
case node.DOCUMENT_TYPE_NODE:

@@ -139,2 +146,23 @@ return serializeDocumentTypeNode(node as DocumentType)

function serializeDocumentFragmentNode(
element: DocumentFragment,
options: SerializeOptions
): DocumentFragmentNode | undefined {
let childNodes: SerializedNodeWithId[] = []
if (element.childNodes.length) {
childNodes = serializeChildNodes(element, options)
}
const isShadowRoot = isNodeShadowRoot(element)
if (isShadowRoot) {
options.serializationContext.shadowRootsController.addShadowRoot(element)
}
return {
type: NodeType.DocumentFragment,
childNodes,
isShadowRoot,
}
}
/**

@@ -204,2 +232,9 @@ * Serializing Element nodes involves capturing:

if (isNodeShadowHost(element) && isExperimentalFeatureEnabled('record_shadow_dom')) {
const shadowRoot = serializeNodeWithId(element.shadowRoot, options)
if (shadowRoot !== null) {
childNodes.push(shadowRoot)
}
}
return {

@@ -243,3 +278,2 @@ type: NodeType.Element,

const result: SerializedNodeWithId[] = []
forEach(node.childNodes, (childNode) => {

@@ -251,3 +285,2 @@ const serializedChildNode = serializeNodeWithId(childNode, options)

})
return result

@@ -254,0 +287,0 @@ }

@@ -268,3 +268,3 @@ import type { HttpRequest, TimeStamp } from '@datadog/browser-core'

describe('computeSegmentContext', () => {
const DEFAULT_VIEW_CONTEXT: ViewContext = { id: '123' }
const DEFAULT_VIEW_CONTEXT: ViewContext = { id: '123', documentVersion: 0 }
const DEFAULT_SESSION = createRumSessionManagerMock().setId('456')

@@ -271,0 +271,0 @@

@@ -66,3 +66,3 @@ /* eslint-disable */

*/
export type SerializedNode = DocumentNode | DocumentTypeNode | ElementNode | TextNode | CDataNode
export type SerializedNode = DocumentNode | DocumentFragmentNode | DocumentTypeNode | ElementNode | TextNode | CDataNode
/**

@@ -91,2 +91,3 @@ * Browser-specific. Schema of a Record type which contains mutations of a screen.

| ViewportResizeData
| PointerInteractionData
/**

@@ -197,2 +198,11 @@ * Browser-specific. Schema of a MutationData.

/**
* Schema of a PointerInteractionData.
*/
export type PointerInteractionData = {
/**
* The source of this type of incremental data.
*/
readonly source: 9
} & PointerInteraction
/**
* Schema of a Record which contains the screen properties.

@@ -384,2 +394,16 @@ */

/**
* Schema of a Document FragmentNode.
*/
export interface DocumentFragmentNode {
/**
* The type of this Node.
*/
readonly type: 11
/**
* Is this node a shadow root or not
*/
readonly isShadowRoot: boolean
childNodes: SerializedNodeWithId[]
}
/**
* Schema of a Document Type Node.

@@ -423,6 +447,2 @@ */

isSVG?: true
/**
* Is this node a host of a shadow root
*/
isShadowHost?: true
}

@@ -649,1 +669,26 @@ /**

}
/**
* Schema of a PointerInteraction.
*/
export interface PointerInteraction {
/**
* Schema of an PointerEventType
*/
readonly pointerEventType: 'down' | 'up' | 'move'
/**
* Schema of an PointerType
*/
readonly pointerType: 'mouse' | 'touch' | 'pen'
/**
* Id of the pointer of this PointerInteraction.
*/
pointerId: number
/**
* X-axis coordinate for this PointerInteraction.
*/
x: number
/**
* Y-axis coordinate for this PointerInteraction.
*/
y: number
}

@@ -29,2 +29,3 @@ import type * as SessionReplay from './sessionReplay'

CDATA: SessionReplay.CDataNode['type']
DocumentFragment: SessionReplay.DocumentFragmentNode['type']
} = {

@@ -36,2 +37,3 @@ Document: 0,

CDATA: 4,
DocumentFragment: 11,
} as const

@@ -38,0 +40,0 @@

Sorry, the diff of this file is too big to display

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

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

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

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