🐛 Keep inlining link stylesheets after a client-side navigation - #5019
Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits intoSep 7, 2026
Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
Conversation
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.
|
✅ All CI checks and tests passed. 🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 9eade67 | Docs | View more details | Give us feedback! |
|
All contributors have signed the CLA ✍️ ✅ |
Contributor
Author
|
I have read the CLA Document and I hereby sign the CLA |
Contributor
Author
|
recheck |
Fhou
marked this pull request as ready for review
September 7, 2026 13:55
sethfowler-datadog
approved these changes
Sep 7, 2026
sethfowler-datadog
left a comment
Contributor
There was a problem hiding this comment.
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), | ||
| } | ||
| } |
Contributor
There was a problem hiding this comment.
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.
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' | ||
|
|
Contributor
There was a problem hiding this comment.
Nit: similarly, it'd make sense to just inline these since they're only used in one test.
Bundles Sizes Evolution
|
gh-worker-dd-mergequeue-cf854d
Bot
deleted the
fhou/fix-relative-link-stylesheet-matching
branch
September 7, 2026 15:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 comparingCSSStyleSheet#hrefwithHTMLLinkElement#href. That comparison breaks after a client-side navigation:CSSStyleSheet#hrefis frozen at load time, whileHTMLLinkElement#hrefre-resolves a relative attribute againstdocument.baseURI, whichpushStatechanges. Once the new path sits in a different directory no stylesheet matches,_cssTextis no longer emitted, and the player falls back to fetching the raw relativehrefre-based on the new view URL, which 404s.Reported on a SvelteKit application, whose
kit.paths.relativedefaults totrueand 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's7.4.0and still atmain.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 fromHTMLLinkElement#sheetinstead of finding it indocument.styleSheetsbyhrefequality. This is the canonical accessor, symmetric with the<style>branch right below it, and immune to base URL changes.&& stylesheetguard, sincegetCssRulesString(null)already returnsnull, and thedoclocal whose only use was the removed lookup.document.styleSheetsis no longer read anywhere in the recorder.serializeAttributes.spec.ts. It loads a real stylesheet through a relativehrefinside 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 thanpushStatebecause asrcdocdocument isabout:srcdoc, wherepushStatethrows; both changedocument.baseURI, which is allHTMLLinkElement#hrefreads.packages/browser-rum/test/record/relativeStylesheet.cssas the served fixture, since the existingtoto.cssis empty and yields nocssText. Registered intest/unit/karma.base.conf.jswithincluded: falseso 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 onlyserializeAttributes.tsfrommainmakes the new spec fail, so the test genuinely pins the regression.browser-rumsuite passes (781 specs). The three existing<link>specs inserializeNode.stylesheet.spec.tsstubtarget.sheetand use absolute hrefs, so they were blind to this and remain green.yarn dev-server: a page served one directory deep, carrying a<link rel="stylesheet">with a relativehrefthat climbs out of that directory, plus a button doing apushStateinto a deeper path. Reading the decoded segments back fromyarn 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.url()inside the inlined CSS stays correct, sincegetCssRulesString()absolutizes againstCSSStyleSheet#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.hrefplus apushStatethat changes directory" would require new fixtures for coverage the unit test already pins in a real browser.Checklist