@datadog/browser-rum
Advanced tools
Comparing version 4.29.0 to 4.29.1
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1543446
227
25910
+ Added@datadog/browser-core@4.29.1(transitive)
+ Added@datadog/browser-logs@4.29.1(transitive)
+ Added@datadog/browser-rum-core@4.29.1(transitive)
- Removed@datadog/browser-core@4.29.0(transitive)
- Removed@datadog/browser-logs@4.29.0(transitive)
- Removed@datadog/browser-rum-core@4.29.0(transitive)
Updated@datadog/browser-core@4.29.1