Skip to content

🐛 Keep inlining link stylesheets after a client-side navigation - #5019

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
mainfrom
fhou/fix-relative-link-stylesheet-matching
Sep 7, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
mainfrom
fhou/fix-relative-link-stylesheet-matching

Conversation

@Fhou

@Fhou Fhou commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Motivation

RUMS-6225: a customer's Session Replay renders correctly on the SSR view, then loses all CSS on the first client-side navigation, with the player 404ing on the stylesheet URLs.

The recorder looked up a <link> element's stylesheet by comparing CSSStyleSheet#href with HTMLLinkElement#href. That comparison breaks after a client-side navigation: CSSStyleSheet#href is frozen at load time, while HTMLLinkElement#href re-resolves a relative attribute against document.baseURI, which pushState changes. Once the new path sits in a different directory no stylesheet matches, _cssText is no longer emitted, and the player falls back to fetching the raw relative href re-based on the new view URL, which 404s.

Reported on a SvelteKit application, whose kit.paths.relative defaults to true and therefore emits relative asset paths during SSR, but this is not framework-specific: any build emitting relative stylesheet paths, combined with a client-side navigation that changes the path directory, is affected. Verified present in the customer's 7.4.0 and still at main.

Since the CSS is absent from the segments that were already recorded, this only affects new recordings: existing sessions cannot be repaired retroactively.

Changes

  • serializeVirtualAttributes() now reads the stylesheet from HTMLLinkElement#sheet instead of finding it in document.styleSheets by href equality. This is the canonical accessor, symmetric with the <style> branch right below it, and immune to base URL changes.
  • Dropped the now-dead && stylesheet guard, since getCssRulesString(null) already returns null, and the doc local whose only use was the removed lookup. document.styleSheets is no longer read anywhere in the recorder.
  • Added a regression test in serializeAttributes.spec.ts. It loads a real stylesheet through a relative href inside an isolated iframe, then rewrites the base URL to a deeper directory. The iframe keeps the base URL change from affecting the rest of the run, and <base> is used rather than pushState because a srcdoc document is about:srcdoc, where pushState throws; both change document.baseURI, which is all HTMLLinkElement#href reads.
  • Added packages/browser-rum/test/record/relativeStylesheet.css as the served fixture, since the existing toto.css is empty and yields no cssText. Registered in test/unit/karma.base.conf.js with included: false so its rules are served without being injected into the runner page.

Test instructions

  • yarn test:unit --spec 'packages/browser-rum/src/domain/record/serialization/serializeAttributes.spec.ts' passes (13 specs). Restoring only serializeAttributes.ts from main makes the new spec fail, so the test genuinely pins the regression.
  • The full browser-rum suite passes (781 specs). The three existing <link> specs in serializeNode.stylesheet.spec.ts stub target.sheet and use absolute hrefs, so they were blind to this and remain green.
  • Verified end to end with yarn dev-server: a page served one directory deep, carrying a <link rel="stylesheet"> with a relative href that climbs out of that directory, plus a button doing a pushState into a deeper path. Reading the decoded segments back from yarn dev-server intake, the post-navigation view carries no CSS at all before the fix, and the same CSS as the initial view after it.
  • Relative url() inside the inlined CSS stays correct, since getCssRulesString() absolutizes against CSSStyleSheet#href, which is the load-time URL. Confirmed on the same repro: the post-navigation view still resolves them against the stylesheet's original directory rather than the shifted one.
  • No e2e test: the Playwright harness serves pages from a fixed path, so reproducing "relative href plus a pushState that changes directory" would require new fixtures for coverage the unit test already pins in a real browser.

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated documentation and/or relevant AGENTS.md file

The recorder looked up a <link> element's stylesheet by comparing CSSStyleSheet#href with HTMLLinkElement#href. That comparison breaks after a client-side navigation: CSSStyleSheet#href is frozen at load time, while HTMLLinkElement#href re-resolves a relative attribute against document.baseURI, which pushState changes. Once the new path sits in a different directory no stylesheet matches, the CSS is no longer embedded in the records, and the player falls back to fetching the raw relative href re-based on the new view URL, which 404s. Use HTMLLinkElement#sheet instead: it is symmetric with the <style> branch below it and immune to navigation.

Reported on a SvelteKit application (RUMS-6225), whose SSR emits relative asset paths by default, but any build doing the same is affected.

The regression test loads a real stylesheet through a relative href inside an isolated iframe, then rewrites the base URL. It needs a served fixture with real rules, hence the new CSS file and its karma registration with included: false so those rules don't leak into other specs.
@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Sep 7, 2026

Copy link
Copy Markdown

Tests

All CI checks and tests passed.

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 50.00%
Overall Coverage: 77.10% (+0.03%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 9eade67 | Docs | View more details | Give us feedback!

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@Fhou

Fhou commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@Fhou

Fhou commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

recheck

@Fhou
Fhou marked this pull request as ready for review September 7, 2026 13:55
@Fhou
Fhou requested review from a team as code owners September 7, 2026 13:55

@sethfowler-datadog sethfowler-datadog left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Some nits below.

Comment on lines +375 to +406
/**
* Appends a really loaded `<link rel="stylesheet">` whose `href` attribute is relative, in an
* isolated iframe so that changing the base URL can't affect the rest of the test run.
*/
async function appendLinkWithRelativeHref(): Promise<{
link: HTMLLinkElement
sheet: CSSStyleSheet
setBaseUrl: (url: string) => void
}> {
const iframe = document.createElement('iframe')
registerCleanupTask(() => {
iframe.remove()
})

const loaded = new Promise((resolve) => iframe.addEventListener('load', resolve))
iframe.srcdoc = `
<base href="${RELATIVE_CSS_BASE_URL}">
<link rel="stylesheet" href="${RELATIVE_CSS_HREF}">
`
document.body.appendChild(iframe)
await loaded

const iframeDocument = iframe.contentDocument!
const link = iframeDocument.querySelector('link')!
expect(link.sheet).withContext('the stylesheet should have loaded').not.toBeNull()

return {
link,
sheet: link.sheet!,
setBaseUrl: (url: string) => iframeDocument.querySelector('base')!.setAttribute('href', url),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is part of the implementation of the specific test you're adding, rather than a generic helper, so it'd probably make sense to just move it into the body of the test itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed here 9eade67

Comment on lines +24 to +28
// A base URL whose directory is '/client/', and a relative href that climbs one level out of
// it to reach the stylesheet served at '/base/packages/...'.
const RELATIVE_CSS_BASE_URL = `${location.origin}/client/new-quote`
const RELATIVE_CSS_HREF = '../base/packages/browser-rum/test/record/relativeStylesheet.css'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: similarly, it'd make sense to just inline these since they're only used in one test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed here 9eade67

@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 182.14 KiB 182.14 KiB 0 B 0.00%
Rum Profiler 8.43 KiB 8.43 KiB 0 B 0.00%
Rum Recorder 28.27 KiB 28.20 KiB -68 B -0.23%
Logs 57.96 KiB 57.96 KiB 0 B 0.00%
Rum Salesforce N/A 140.20 KiB N/A N/A N/A
Rum Slim 140.20 KiB 140.20 KiB 0 B 0.00%
Worker 22.96 KiB 22.96 KiB 0 B 0.00%
Rum Shopify N/A 209.84 KiB N/A N/A N/A
Rum-shopify Profiler N/A 8.43 KiB N/A N/A N/A
Rum-shopify Recorder N/A 3.74 KiB N/A N/A N/A

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit d4c807a into main Sep 7, 2026
32 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the fhou/fix-relative-link-stylesheet-matching branch September 7, 2026 15:11
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants