From 2327ceb40afb345e95906acbcc1ae6da7bdde56d Mon Sep 17 00:00:00 2001 From: Andrew Pomeroy Date: Mon, 11 Dec 2023 17:22:23 -0800 Subject: [PATCH 1/5] feat(Snapshot): Capture stylesheets designated as `rel="preload"` --- packages/rrweb-snapshot/src/snapshot.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 301f84430e..7c36f49582 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -1174,11 +1174,12 @@ export function serializeNodeWithId( } // - if ( - serializedNode.type === NodeType.Element && + if (serializedNode.type === NodeType.Element && serializedNode.tagName === 'link' && - serializedNode.attributes.rel === 'stylesheet' - ) { + typeof serializedNode.attributes.rel === 'string' && + (serializedNode.attributes.rel === 'stylesheet' || (serializedNode.attributes.rel === 'preload' && + typeof serializedNode.attributes.href === 'string' && + serializedNode.attributes.href.endsWith('.css')))) { onceStylesheetLoaded( n as HTMLLinkElement, () => { From 3a8a2972527a6ad3dc1cc09ebdd0c6f893db6a5a Mon Sep 17 00:00:00 2001 From: Andrew Pomeroy Date: Mon, 11 Dec 2023 19:19:46 -0800 Subject: [PATCH 2/5] fix(Snapshot): Harden asset file extension matching --- packages/rrweb-snapshot/src/snapshot.ts | 5 ++- packages/rrweb-snapshot/src/utils.ts | 13 ++++++ packages/rrweb-snapshot/test/utils.test.ts | 46 +++++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 7c36f49582..b955b047aa 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -23,6 +23,7 @@ import { stringifyStylesheet, getInputType, toLowerCase, + extractFileExtension, } from './utils'; let _id = 1; @@ -847,7 +848,7 @@ function slimDOMExcluded( (sn.tagName === 'link' && sn.attributes.rel === 'prefetch' && typeof sn.attributes.href === 'string' && - sn.attributes.href.endsWith('.js'))) + extractFileExtension(sn.attributes.href) === 'js')) ) { return true; } else if ( @@ -1179,7 +1180,7 @@ export function serializeNodeWithId( typeof serializedNode.attributes.rel === 'string' && (serializedNode.attributes.rel === 'stylesheet' || (serializedNode.attributes.rel === 'preload' && typeof serializedNode.attributes.href === 'string' && - serializedNode.attributes.href.endsWith('.css')))) { + extractFileExtension(serializedNode.attributes.href) === 'css'))) { onceStylesheetLoaded( n as HTMLLinkElement, () => { diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 95444c18b3..d7d04f43ee 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -331,3 +331,16 @@ export function getInputType(element: HTMLElement): Lowercase | null { toLowerCase(type) : null; } + +/** + * Extracts the file extension from an a path, considering search parameters and fragments. + * @param path Path to file + * @param [baseURL] Base URL of the page, used to resolve relative paths. Defaults to current page URL. + */ +export function extractFileExtension(path: string, baseURL?: string): string | null { + const url = new URL(path, baseURL ?? window.location.href); + const regex = /\.([0-9a-z]+)(?:[\?#]|$)/i; + const match = url.pathname.match(regex); + console.log(url.pathname) + return match?.[1] ?? null; +} diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts index 09a2bebd4d..4202410cfa 100644 --- a/packages/rrweb-snapshot/test/utils.test.ts +++ b/packages/rrweb-snapshot/test/utils.test.ts @@ -2,7 +2,7 @@ * @jest-environment jsdom */ import { NodeType, serializedNode } from '../src/types'; -import { isNodeMetaEqual } from '../src/utils'; +import { extractFileExtension, isNodeMetaEqual } from '../src/utils'; import { serializedNodeWithId } from 'rrweb-snapshot'; describe('utils', () => { @@ -147,4 +147,48 @@ describe('utils', () => { expect(isNodeMetaEqual(element2, element3)).toBeFalsy(); }); }); + describe('extractFileExtension', () => { + test('absolute path', () => { + const path = 'https://example.com/styles/main.css'; + const extension = extractFileExtension(path); + expect(extension).toBe('css'); + }); + + test('relative path', () => { + const path = 'styles/main.css'; + const baseURL = 'https://example.com/'; + const extension = extractFileExtension(path, baseURL); + expect(extension).toBe('css'); + }); + + test('path with search parameters', () => { + const path = 'https://example.com/scripts/app.js?version=1.0'; + const extension = extractFileExtension(path); + expect(extension).toBe('js'); + }); + + test('path with fragment', () => { + const path = 'https://example.com/styles/main.css#section1'; + const extension = extractFileExtension(path); + expect(extension).toBe('css'); + }); + + test('path with search parameters and fragment', () => { + const path = 'https://example.com/scripts/app.js?version=1.0#section1'; + const extension = extractFileExtension(path); + expect(extension).toBe('js'); + }); + + test('path without extension', () => { + const path = 'https://example.com/path/to/directory/'; + const extension = extractFileExtension(path); + expect(extension).toBeNull(); + }); + + test('path with multiple dots', () => { + const path = 'https://example.com/scripts/app.min.js?version=1.0'; + const extension = extractFileExtension(path); + expect(extension).toBe('js'); + }); + }); }); From af1f452f37485e49a05be39d6e335211b46b6260 Mon Sep 17 00:00:00 2001 From: Andrew Pomeroy Date: Mon, 11 Dec 2023 19:48:18 -0800 Subject: [PATCH 3/5] Add changeset --- .changeset/smooth-papayas-boil.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/smooth-papayas-boil.md diff --git a/.changeset/smooth-papayas-boil.md b/.changeset/smooth-papayas-boil.md new file mode 100644 index 0000000000..aa733576ce --- /dev/null +++ b/.changeset/smooth-papayas-boil.md @@ -0,0 +1,6 @@ +--- +"rrweb-snapshot": patch +"rrweb": patch +--- + +Capture stylesheets designated as `rel="preload"` From 83c7af7db65412d4a9e371c3064fa39e07d8d628 Mon Sep 17 00:00:00 2001 From: Andrew Pomeroy Date: Mon, 11 Dec 2023 20:15:18 -0800 Subject: [PATCH 4/5] chore: Lint --- .changeset/smooth-papayas-boil.md | 4 ++-- packages/rrweb-snapshot/src/snapshot.ts | 11 +++++++---- packages/rrweb-snapshot/src/utils.ts | 12 +++++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.changeset/smooth-papayas-boil.md b/.changeset/smooth-papayas-boil.md index aa733576ce..dcf4d36899 100644 --- a/.changeset/smooth-papayas-boil.md +++ b/.changeset/smooth-papayas-boil.md @@ -1,6 +1,6 @@ --- -"rrweb-snapshot": patch -"rrweb": patch +'rrweb-snapshot': patch +'rrweb': patch --- Capture stylesheets designated as `rel="preload"` diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index b955b047aa..75fd863e0b 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -1175,12 +1175,15 @@ export function serializeNodeWithId( } // - if (serializedNode.type === NodeType.Element && + if ( + serializedNode.type === NodeType.Element && serializedNode.tagName === 'link' && typeof serializedNode.attributes.rel === 'string' && - (serializedNode.attributes.rel === 'stylesheet' || (serializedNode.attributes.rel === 'preload' && - typeof serializedNode.attributes.href === 'string' && - extractFileExtension(serializedNode.attributes.href) === 'css'))) { + (serializedNode.attributes.rel === 'stylesheet' || + (serializedNode.attributes.rel === 'preload' && + typeof serializedNode.attributes.href === 'string' && + extractFileExtension(serializedNode.attributes.href) === 'css')) + ) { onceStylesheetLoaded( n as HTMLLinkElement, () => { diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index d7d04f43ee..4d91a63f7b 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -334,13 +334,15 @@ export function getInputType(element: HTMLElement): Lowercase | null { /** * Extracts the file extension from an a path, considering search parameters and fragments. - * @param path Path to file - * @param [baseURL] Base URL of the page, used to resolve relative paths. Defaults to current page URL. + * @param path - Path to file + * @param baseURL - [optional] Base URL of the page, used to resolve relative paths. Defaults to current page URL. */ -export function extractFileExtension(path: string, baseURL?: string): string | null { +export function extractFileExtension( + path: string, + baseURL?: string, +): string | null { const url = new URL(path, baseURL ?? window.location.href); - const regex = /\.([0-9a-z]+)(?:[\?#]|$)/i; + const regex = /\.([0-9a-z]+)(?:[?#]|$)/i; const match = url.pathname.match(regex); - console.log(url.pathname) return match?.[1] ?? null; } From 479e05a3dfb46ed8ad4dd10b4ddb715e1c6ce898 Mon Sep 17 00:00:00 2001 From: Andrew Pomeroy Date: Fri, 15 Dec 2023 11:56:15 -0800 Subject: [PATCH 5/5] Tweak regex, add try-catch block on URL constructor --- packages/rrweb-snapshot/src/utils.ts | 9 +++++++-- packages/rrweb-snapshot/test/utils.test.ts | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 4d91a63f7b..5ccc9082ed 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -341,8 +341,13 @@ export function extractFileExtension( path: string, baseURL?: string, ): string | null { - const url = new URL(path, baseURL ?? window.location.href); - const regex = /\.([0-9a-z]+)(?:[?#]|$)/i; + let url; + try { + url = new URL(path, baseURL ?? window.location.href); + } catch (err) { + return null; + } + const regex = /\.([0-9a-z]+)(?:$)/i; const match = url.pathname.match(regex); return match?.[1] ?? null; } diff --git a/packages/rrweb-snapshot/test/utils.test.ts b/packages/rrweb-snapshot/test/utils.test.ts index 4202410cfa..afbdda2f42 100644 --- a/packages/rrweb-snapshot/test/utils.test.ts +++ b/packages/rrweb-snapshot/test/utils.test.ts @@ -185,6 +185,13 @@ describe('utils', () => { expect(extension).toBeNull(); }); + test('invalid URL', () => { + const path = '!@#$%^&*()'; + const baseURL = 'invalid'; + const extension = extractFileExtension(path, baseURL); + expect(extension).toBeNull(); + }); + test('path with multiple dots', () => { const path = 'https://example.com/scripts/app.min.js?version=1.0'; const extension = extractFileExtension(path);