diff --git a/packages/rum/src/domain/record/serialize.spec.ts b/packages/rum/src/domain/record/serialize.spec.ts index b5668eb30f..508db0c15a 100644 --- a/packages/rum/src/domain/record/serialize.spec.ts +++ b/packages/rum/src/domain/record/serialize.spec.ts @@ -595,6 +595,36 @@ describe('serializeNodeWithId', () => { childNodes: [], }) }) + + it('does not serialize style node with dynamic CSS that is behind CORS', () => { + const linkNode = document.createElement('link') + linkNode.setAttribute('rel', 'stylesheet') + linkNode.setAttribute('href', 'https://datadoghq.com/some/style.css') + isolatedDom.document.head.appendChild(linkNode) + class FakeCSSStyleSheet { + get cssRules() { + return [] + } + } + const styleSheet = new FakeCSSStyleSheet() + spyOnProperty(styleSheet, 'cssRules', 'get').and.throwError(new DOMException('cors issue', 'SecurityError')) + + Object.defineProperty(isolatedDom.document, 'styleSheets', { + value: [styleSheet], + }) + + expect(serializeNodeWithId(linkNode, DEFAULT_OPTIONS)).toEqual({ + type: NodeType.Element, + tagName: 'link', + id: jasmine.any(Number) as unknown as number, + isSVG: undefined, + attributes: { + rel: 'stylesheet', + href: 'https://datadoghq.com/some/style.css', + }, + childNodes: [], + }) + }) }) describe('text nodes serialization', () => { diff --git a/packages/rum/src/domain/record/serialize.ts b/packages/rum/src/domain/record/serialize.ts index ac41133f9f..c03c117a6c 100644 --- a/packages/rum/src/domain/record/serialize.ts +++ b/packages/rum/src/domain/record/serialize.ts @@ -356,18 +356,21 @@ function getValidTagName(tagName: string): string { return processedTagName } -function getCssRulesString(s: CSSStyleSheet): string | null { - try { - const rules = s.rules || s.cssRules - if (rules) { - const styleSheetCssText = Array.from(rules, getCssRuleString).join('') - return switchToAbsoluteUrl(styleSheetCssText, s.href) - } - +function getCssRulesString(cssStyleSheet: CSSStyleSheet | undefined | null): string | null { + if (!cssStyleSheet) { return null - } catch (error) { + } + let rules: CSSRuleList | undefined + try { + rules = cssStyleSheet.rules || cssStyleSheet.cssRules + } catch { + // if css is protected by CORS we cannot access cssRules see: https://www.w3.org/TR/cssom-1/#the-cssstylesheet-interface + } + if (!rules) { return null } + const styleSheetCssText = Array.from(rules, getCssRuleString).join('') + return switchToAbsoluteUrl(styleSheetCssText, cssStyleSheet.href) } function getCssRuleString(rule: CSSRule): string { @@ -428,7 +431,7 @@ function getAttributesForPrivacyLevel( // remote css if (tagName === 'link') { const stylesheet = Array.from(doc.styleSheets).find((s) => s.href === (element as HTMLLinkElement).href) - const cssText = getCssRulesString(stylesheet as CSSStyleSheet) + const cssText = getCssRulesString(stylesheet) if (cssText && stylesheet) { safeAttrs._cssText = cssText } @@ -441,7 +444,7 @@ function getAttributesForPrivacyLevel( // TODO: Currently we only try to get dynamic stylesheet when it is an empty style element !((element as HTMLStyleElement).innerText || element.textContent || '').trim().length ) { - const cssText = getCssRulesString((element as HTMLStyleElement).sheet as CSSStyleSheet) + const cssText = getCssRulesString((element as HTMLStyleElement).sheet) if (cssText) { safeAttrs._cssText = cssText }