diff --git a/packages/core/src/tools/browserDetection.ts b/packages/core/src/tools/browserDetection.ts index dec8f3196c..60960f226a 100644 --- a/packages/core/src/tools/browserDetection.ts +++ b/packages/core/src/tools/browserDetection.ts @@ -5,3 +5,7 @@ export function isIE() { export function isChromium() { return !!(window as any).chrome || /HeadlessChrome/.test(window.navigator.userAgent) } + +export function isAdoptedStyleSheetsSupported() { + return Boolean((document as any).adoptedStyleSheets) +} diff --git a/packages/rum/src/domain/record/browser.types.ts b/packages/rum/src/domain/record/browser.types.ts new file mode 100644 index 0000000000..77ba9ab3fd --- /dev/null +++ b/packages/rum/src/domain/record/browser.types.ts @@ -0,0 +1,12 @@ +// TODO: remove this once typescript has been update to 4.8+ +declare global { + interface ShadowRoot { + adoptedStyleSheets: CSSStyleSheet[] | undefined + } + + interface Document { + adoptedStyleSheets: CSSStyleSheet[] | undefined + } +} + +export {} diff --git a/packages/rum/src/domain/record/serializationUtils.spec.ts b/packages/rum/src/domain/record/serializationUtils.spec.ts index bd709a9c18..5be68f5aa4 100644 --- a/packages/rum/src/domain/record/serializationUtils.spec.ts +++ b/packages/rum/src/domain/record/serializationUtils.spec.ts @@ -1,4 +1,4 @@ -import { isIE } from '@datadog/browser-core' +import { isAdoptedStyleSheetsSupported, isIE } from '@datadog/browser-core' import { NodePrivacyLevel } from '../../constants' import { getSerializedNodeId, @@ -6,6 +6,7 @@ import { setSerializedNodeId, getElementInputValue, switchToAbsoluteUrl, + getStyleSheets, } from './serializationUtils' describe('serialized Node storage in DOM Nodes', () => { @@ -183,3 +184,27 @@ describe('switchToAbsoluteUrl', () => { }) }) }) + +describe('getStyleSheets', () => { + beforeEach(() => { + if (!isAdoptedStyleSheetsSupported()) { + pending('no adoptedStyleSheets support') + } + }) + it('should return undefined if no stylesheets', () => { + expect(getStyleSheets(undefined)).toBe(undefined) + expect(getStyleSheets([])).toBe(undefined) + }) + + it('should return serialized stylesheet', () => { + const disabledStylesheet = new CSSStyleSheet({ disabled: true }) + disabledStylesheet.insertRule('div { width: 100%; }') + const printStylesheet = new CSSStyleSheet({ disabled: false, media: 'print' }) + printStylesheet.insertRule('a { color: red; }') + + expect(getStyleSheets([disabledStylesheet, printStylesheet])).toEqual([ + { cssRules: ['div { width: 100%; }'], disabled: true, media: [] }, + { cssRules: ['a { color: red; }'], disabled: false, media: ['print'] }, + ]) + }) +}) diff --git a/packages/rum/src/domain/record/serializationUtils.ts b/packages/rum/src/domain/record/serializationUtils.ts index 988b88699e..9703dcfbb5 100644 --- a/packages/rum/src/domain/record/serializationUtils.ts +++ b/packages/rum/src/domain/record/serializationUtils.ts @@ -2,6 +2,7 @@ import { buildUrl } from '@datadog/browser-core' import { getParentNode, isNodeShadowRoot } from '@datadog/browser-rum-core' import type { NodePrivacyLevel } from '../../constants' import { CENSORED_STRING_MARK } from '../../constants' +import type { StyleSheet } from '../../types' import { shouldMaskNode } from './privacy' export type NodeWithSerializedNode = Node & { s: 'Node with serialized node' } @@ -106,3 +107,20 @@ export function makeUrlAbsolute(url: string, baseUrl: string): string { return url } } + +export function getStyleSheets(cssStyleSheets: CSSStyleSheet[] | undefined): StyleSheet[] | undefined { + if (cssStyleSheets === undefined || cssStyleSheets.length === 0) { + return undefined + } + return cssStyleSheets.map((cssStyleSheet) => { + const rules = cssStyleSheet.rules || cssStyleSheet.cssRules + const cssRules = Array.from(rules, (cssRule) => cssRule.cssText) + + const styleSheet: StyleSheet = { + cssRules, + disabled: cssStyleSheet.disabled, + media: Array.from(cssStyleSheet.media), + } + return styleSheet + }) +} diff --git a/packages/rum/src/domain/record/serialize.spec.ts b/packages/rum/src/domain/record/serialize.spec.ts index 363f45a062..b8cab883d1 100644 --- a/packages/rum/src/domain/record/serialize.spec.ts +++ b/packages/rum/src/domain/record/serialize.spec.ts @@ -86,6 +86,7 @@ describe('serializeNodeWithId', () => { jasmine.objectContaining({ type: NodeType.DocumentType, name: 'html', publicId: '', systemId: '' }), jasmine.objectContaining({ type: NodeType.Element, tagName: 'html' }), ], + adoptedStyleSheets: undefined, id: jasmine.any(Number) as unknown as number, }) }) @@ -437,6 +438,7 @@ describe('serializeNodeWithId', () => { isShadowRoot: true, childNodes: [], id: jasmine.any(Number) as unknown as number, + adoptedStyleSheets: undefined, }, ], id: jasmine.any(Number) as unknown as number, @@ -468,6 +470,7 @@ describe('serializeNodeWithId', () => { { type: NodeType.DocumentFragment, isShadowRoot: true, + adoptedStyleSheets: undefined, childNodes: [ { type: NodeType.Element, @@ -692,6 +695,7 @@ describe('serializeDocumentNode handles', function testAllowDomTree() { expect(serializeDocumentNode(document, serializeOptionsMask)).toEqual({ type: NodeType.Document, childNodes: serializeChildNodes(document, serializeOptionsMask), + adoptedStyleSheets: undefined, }) }) diff --git a/packages/rum/src/domain/record/serialize.ts b/packages/rum/src/domain/record/serialize.ts index 31919dde75..a56389f633 100644 --- a/packages/rum/src/domain/record/serialize.ts +++ b/packages/rum/src/domain/record/serialize.ts @@ -31,10 +31,12 @@ import { setSerializedNodeId, getElementInputValue, switchToAbsoluteUrl, + getStyleSheets, } from './serializationUtils' import { forEach } from './utils' import type { ElementsScrollPositions } from './elementsScrollPositions' import type { ShadowRootsController } from './shadowRootsController' +import './browser.types' // Those values are the only one that can be used when inheriting privacy levels from parent to // children during serialization, since HIDDEN and IGNORE shouldn't serialize their children. This @@ -125,6 +127,7 @@ export function serializeDocumentNode(document: Document, options: SerializeOpti return { type: NodeType.Document, childNodes: serializeChildNodes(document, options), + adoptedStyleSheets: getStyleSheets(document.adoptedStyleSheets), } } @@ -155,6 +158,7 @@ function serializeDocumentFragmentNode( type: NodeType.DocumentFragment, childNodes, isShadowRoot, + adoptedStyleSheets: isShadowRoot ? getStyleSheets(element.adoptedStyleSheets) : undefined, } }