Skip to content

Commit 314a8dd

Browse files
Capture stylesheets designated as rel="preload" (rrweb-io#1374)
* feat(Snapshot): Capture stylesheets designated as `rel="preload"` * fix(Snapshot): Harden asset file extension matching * Add changeset * chore: Lint * Tweak regex, add try-catch block on URL constructor
1 parent 5844f60 commit 314a8dd

File tree

4 files changed

+85
-3
lines changed

4 files changed

+85
-3
lines changed

.changeset/smooth-papayas-boil.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'rrweb-snapshot': patch
3+
'rrweb': patch
4+
---
5+
6+
Capture stylesheets designated as `rel="preload"`

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
stringifyStylesheet,
2424
getInputType,
2525
toLowerCase,
26+
extractFileExtension,
2627
} from './utils';
2728

2829
let _id = 1;
@@ -847,7 +848,7 @@ function slimDOMExcluded(
847848
(sn.tagName === 'link' &&
848849
sn.attributes.rel === 'prefetch' &&
849850
typeof sn.attributes.href === 'string' &&
850-
sn.attributes.href.endsWith('.js')))
851+
extractFileExtension(sn.attributes.href) === 'js'))
851852
) {
852853
return true;
853854
} else if (
@@ -1177,7 +1178,11 @@ export function serializeNodeWithId(
11771178
if (
11781179
serializedNode.type === NodeType.Element &&
11791180
serializedNode.tagName === 'link' &&
1180-
serializedNode.attributes.rel === 'stylesheet'
1181+
typeof serializedNode.attributes.rel === 'string' &&
1182+
(serializedNode.attributes.rel === 'stylesheet' ||
1183+
(serializedNode.attributes.rel === 'preload' &&
1184+
typeof serializedNode.attributes.href === 'string' &&
1185+
extractFileExtension(serializedNode.attributes.href) === 'css'))
11811186
) {
11821187
onceStylesheetLoaded(
11831188
n as HTMLLinkElement,

packages/rrweb-snapshot/src/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,23 @@ export function getInputType(element: HTMLElement): Lowercase<string> | null {
331331
toLowerCase(type)
332332
: null;
333333
}
334+
335+
/**
336+
* Extracts the file extension from an a path, considering search parameters and fragments.
337+
* @param path - Path to file
338+
* @param baseURL - [optional] Base URL of the page, used to resolve relative paths. Defaults to current page URL.
339+
*/
340+
export function extractFileExtension(
341+
path: string,
342+
baseURL?: string,
343+
): string | null {
344+
let url;
345+
try {
346+
url = new URL(path, baseURL ?? window.location.href);
347+
} catch (err) {
348+
return null;
349+
}
350+
const regex = /\.([0-9a-z]+)(?:$)/i;
351+
const match = url.pathname.match(regex);
352+
return match?.[1] ?? null;
353+
}

packages/rrweb-snapshot/test/utils.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @jest-environment jsdom
33
*/
44
import { NodeType, serializedNode } from '../src/types';
5-
import { isNodeMetaEqual } from '../src/utils';
5+
import { extractFileExtension, isNodeMetaEqual } from '../src/utils';
66
import { serializedNodeWithId } from 'rrweb-snapshot';
77

88
describe('utils', () => {
@@ -147,4 +147,55 @@ describe('utils', () => {
147147
expect(isNodeMetaEqual(element2, element3)).toBeFalsy();
148148
});
149149
});
150+
describe('extractFileExtension', () => {
151+
test('absolute path', () => {
152+
const path = 'https://example.com/styles/main.css';
153+
const extension = extractFileExtension(path);
154+
expect(extension).toBe('css');
155+
});
156+
157+
test('relative path', () => {
158+
const path = 'styles/main.css';
159+
const baseURL = 'https://example.com/';
160+
const extension = extractFileExtension(path, baseURL);
161+
expect(extension).toBe('css');
162+
});
163+
164+
test('path with search parameters', () => {
165+
const path = 'https://example.com/scripts/app.js?version=1.0';
166+
const extension = extractFileExtension(path);
167+
expect(extension).toBe('js');
168+
});
169+
170+
test('path with fragment', () => {
171+
const path = 'https://example.com/styles/main.css#section1';
172+
const extension = extractFileExtension(path);
173+
expect(extension).toBe('css');
174+
});
175+
176+
test('path with search parameters and fragment', () => {
177+
const path = 'https://example.com/scripts/app.js?version=1.0#section1';
178+
const extension = extractFileExtension(path);
179+
expect(extension).toBe('js');
180+
});
181+
182+
test('path without extension', () => {
183+
const path = 'https://example.com/path/to/directory/';
184+
const extension = extractFileExtension(path);
185+
expect(extension).toBeNull();
186+
});
187+
188+
test('invalid URL', () => {
189+
const path = '!@#$%^&*()';
190+
const baseURL = 'invalid';
191+
const extension = extractFileExtension(path, baseURL);
192+
expect(extension).toBeNull();
193+
});
194+
195+
test('path with multiple dots', () => {
196+
const path = 'https://example.com/scripts/app.min.js?version=1.0';
197+
const extension = extractFileExtension(path);
198+
expect(extension).toBe('js');
199+
});
200+
});
150201
});

0 commit comments

Comments
 (0)