Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REPLAY] Add support for adoptedStyleSheets #1916

Merged
merged 14 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/src/tools/browserDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions packages/rum-core/test/createIsolatedDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export function createIsolatedDom() {
element(s: TemplateStringsArray) {
return append(s[0])
},
doc,
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
window: iframe.contentWindow! as Window & { CSSStyleSheet: typeof CSSStyleSheet },
append,
clear() {
iframe.parentNode!.removeChild(iframe)
Expand Down
12 changes: 12 additions & 0 deletions packages/rum/src/domain/record/browser.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// TODO: remove this once typescript has been update to 4.8+
declare global {
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
interface ShadowRoot {
adoptedStyleSheets: CSSStyleSheet[] | undefined
}

interface Document {
adoptedStyleSheets: CSSStyleSheet[] | undefined
}
}

export {}
27 changes: 26 additions & 1 deletion packages/rum/src/domain/record/serializationUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { isIE } from '@datadog/browser-core'
import { isAdoptedStyleSheetsSupported, isIE } from '@datadog/browser-core'
import { NodePrivacyLevel } from '../../constants'
import {
getSerializedNodeId,
hasSerializedNode,
setSerializedNodeId,
getElementInputValue,
switchToAbsoluteUrl,
getStyleSheets,
} from './serializationUtils'

describe('serialized Node storage in DOM Nodes', () => {
Expand Down Expand Up @@ -183,3 +184,27 @@ describe('switchToAbsoluteUrl', () => {
})
})
})

describe('getStyleSheets', () => {
ThibautGeriz marked this conversation as resolved.
Show resolved Hide resolved
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: undefined },
{ cssRules: ['a { color: red; }'], disabled: undefined, media: ['print'] },
])
})
})
18 changes: 18 additions & 0 deletions packages/rum/src/domain/record/serializationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down Expand Up @@ -106,3 +107,20 @@ export function makeUrlAbsolute(url: string, baseUrl: string): string {
return url
}
}

export function getStyleSheets(cssStyleSheets: CSSStyleSheet[] | undefined): StyleSheet[] | undefined {
ThibautGeriz marked this conversation as resolved.
Show resolved Hide resolved
if (cssStyleSheets === undefined || cssStyleSheets.length === 0) {
return undefined
}
return cssStyleSheets.map((cssStyleSheet) => {
const rules = cssStyleSheet.rules || cssStyleSheet.cssRules
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
const cssRules = Array.from(rules, (cssRule) => cssRule.cssText)

const styleSheet: StyleSheet = {
cssRules,
disabled: cssStyleSheet.disabled || undefined,
media: cssStyleSheet.media.length > 0 ? Array.from(cssStyleSheet.media) : undefined,
}
return styleSheet
})
}
37 changes: 36 additions & 1 deletion packages/rum/src/domain/record/serialize.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { isIE, noop, resetExperimentalFeatures, updateExperimentalFeatures } from '@datadog/browser-core'
import {
isAdoptedStyleSheetsSupported,
isIE,
noop,
resetExperimentalFeatures,
updateExperimentalFeatures,
} from '@datadog/browser-core'
import type { RumConfiguration } from '@datadog/browser-rum-core'
import { STABLE_ATTRIBUTES, DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE } from '@datadog/browser-rum-core'
import {
Expand All @@ -19,6 +25,7 @@ import {
} from '../../../test/htmlAst'
import type { ElementNode, SerializedNodeWithId, TextNode } from '../../types'
import { NodeType } from '../../types'
import { createIsolatedDom } from '../../../../rum-core/test/createIsolatedDom'
import { hasSerializedNode } from './serializationUtils'
import type { SerializationContext, SerializeOptions } from './serialize'
import {
Expand Down Expand Up @@ -86,6 +93,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,
})
})
Expand Down Expand Up @@ -437,6 +445,7 @@ describe('serializeNodeWithId', () => {
isShadowRoot: true,
childNodes: [],
id: jasmine.any(Number) as unknown as number,
adoptedStyleSheets: undefined,
},
],
id: jasmine.any(Number) as unknown as number,
Expand Down Expand Up @@ -468,6 +477,7 @@ describe('serializeNodeWithId', () => {
{
type: NodeType.DocumentFragment,
isShadowRoot: true,
adoptedStyleSheets: undefined,
childNodes: [
{
type: NodeType.Element,
Expand Down Expand Up @@ -684,6 +694,30 @@ describe('serializeDocumentNode handles', function testAllowDomTree() {
}
})

it('serializes a document with adoptedStyleSheets', () => {
if (!isAdoptedStyleSheetsSupported()) {
pending('no adoptedStyleSheets support')
}
const isolatedDom = createIsolatedDom()
const styleSheet = new isolatedDom.window.CSSStyleSheet()
styleSheet.insertRule('div { width: 100%; }')
isolatedDom.doc.adoptedStyleSheets = [styleSheet]
expect(serializeDocument(isolatedDom.doc, DEFAULT_CONFIGURATION, DEFAULT_SERIALIZATION_CONTEXT)).toEqual({
type: NodeType.Document,
childNodes: [jasmine.objectContaining({ type: NodeType.Element, tagName: 'html' })],
adoptedStyleSheets: [
{
cssRules: ['div { width: 100%; }'],
disabled: undefined,
media: undefined,
},
],
id: jasmine.any(Number) as unknown as number,
})

isolatedDom.clear()
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
})

it('a masked DOM Document itself is still serialized ', () => {
const serializeOptionsMask: SerializeOptions = {
...DEFAULT_OPTIONS,
Expand All @@ -692,6 +726,7 @@ describe('serializeDocumentNode handles', function testAllowDomTree() {
expect(serializeDocumentNode(document, serializeOptionsMask)).toEqual({
type: NodeType.Document,
childNodes: serializeChildNodes(document, serializeOptionsMask),
adoptedStyleSheets: undefined,
})
})

Expand Down
4 changes: 4 additions & 0 deletions packages/rum/src/domain/record/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -125,6 +127,7 @@ export function serializeDocumentNode(document: Document, options: SerializeOpti
return {
type: NodeType.Document,
childNodes: serializeChildNodes(document, options),
adoptedStyleSheets: getStyleSheets(document.adoptedStyleSheets),
}
}

Expand Down Expand Up @@ -155,6 +158,7 @@ function serializeDocumentFragmentNode(
type: NodeType.DocumentFragment,
childNodes,
isShadowRoot,
adoptedStyleSheets: isShadowRoot ? getStyleSheets(element.adoptedStyleSheets) : undefined,
}
}

Expand Down