From 88bb79ef5594d5b9b2391746dfa820b152a5b2f8 Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 4 Jan 2022 16:16:22 -0800 Subject: [PATCH 1/4] Add diffs --- src/replay/index.ts | 111 ++++++++++++++++++++++++++------------------ src/replay/timer.ts | 1 + 2 files changed, 66 insertions(+), 46 deletions(-) diff --git a/src/replay/index.ts b/src/replay/index.ts index 9d722d18..67768218 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -110,6 +110,7 @@ export class Replayer { private emitter: Emitter = mitt(); + private nextUserInteractionEvent: eventWithTime | null; private activityIntervals: Array = []; private inactiveEndTimestamp: number | null; @@ -127,9 +128,6 @@ export class Replayer { private imageMap: Map = new Map(); - /** The first time the player is playing. */ - private nextUserInteractionEvent: eventWithTime | null; - private mirror: Mirror = createMirror(); private firstFullSnapshot: eventWithTime | true | null = null; @@ -555,7 +553,7 @@ export class Replayer { this.iframe.contentDocument, ); - polyfill(this.iframe.contentWindow as Window & typeof globalThis); + polyfill(this.iframe.contentWindow as IWindow); } } @@ -630,12 +628,11 @@ export class Replayer { }; break; case EventType.Meta: - castFn = () => { + castFn = () => this.emitter.emit(ReplayerEvents.Resize, { width: event.data.width, height: event.data.height, }); - }; break; case EventType.FullSnapshot: castFn = () => { @@ -894,37 +891,6 @@ export class Replayer { } } - private hasImageArg(args: any[]): boolean { - for (const arg of args) { - if (!arg || typeof arg !== 'object') { - // do nothing - } else if ('rr_type' in arg && 'args' in arg) { - if (this.hasImageArg(arg.args)) return true; - } else if ('rr_type' in arg && arg.rr_type === 'HTMLImageElement') { - return true; // has image! - } else if (arg instanceof Array) { - if (this.hasImageArg(arg)) return true; - } - } - return false; - } - - private getImageArgs(args: any[]): string[] { - const images: string[] = []; - for (const arg of args) { - if (!arg || typeof arg !== 'object') { - // do nothing - } else if ('rr_type' in arg && 'args' in arg) { - images.push(...this.getImageArgs(arg.args)); - } else if ('rr_type' in arg && arg.rr_type === 'HTMLImageElement') { - images.push(arg.src); - } else if (arg instanceof Array) { - images.push(...this.getImageArgs(arg)); - } - } - return images; - } - /** * pause when loading style sheet, resume when loaded all timeout exceed */ @@ -981,6 +947,37 @@ export class Replayer { } } + private hasImageArg(args: any[]): boolean { + for (const arg of args) { + if (!arg || typeof arg !== 'object') { + // do nothing + } else if ('rr_type' in arg && 'args' in arg) { + if (this.hasImageArg(arg.args)) return true; + } else if ('rr_type' in arg && arg.rr_type === 'HTMLImageElement') { + return true; // has image! + } else if (arg instanceof Array) { + if (this.hasImageArg(arg)) return true; + } + } + return false; + } + + private getImageArgs(args: any[]): string[] { + const images: string[] = []; + for (const arg of args) { + if (!arg || typeof arg !== 'object') { + // do nothing + } else if ('rr_type' in arg && 'args' in arg) { + images.push(...this.getImageArgs(arg.args)); + } else if ('rr_type' in arg && arg.rr_type === 'HTMLImageElement') { + images.push(arg.src); + } else if (arg instanceof Array) { + images.push(...this.getImageArgs(arg)); + } + } + return images; + } + /** * pause when there are some canvas drawImage args need to be loaded */ @@ -991,8 +988,6 @@ export class Replayer { }; this.emitter.on(ReplayerEvents.Start, stateHandler); this.emitter.on(ReplayerEvents.Pause, stateHandler); - let count = 0; - let resolved = 0; for (const event of this.service.state.context.events) { if ( event.type === EventType.IncrementalSnapshot && @@ -1001,7 +996,6 @@ export class Replayer { typeof event.data.args[0] === 'string' && !this.imageMap.has(event) ) { - count++; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const imgd = ctx?.createImageData(canvas.width, canvas.height); @@ -1020,9 +1014,6 @@ export class Replayer { }); } } - if (count !== resolved) { - this.service.send({ type: 'PAUSE' }); - } } private applyIncremental( @@ -1393,6 +1384,7 @@ export class Replayer { if (!target) { return this.debugNodeNotFound(d, d.id); } + canvasMutation({ event: e, mutation: d, @@ -1400,6 +1392,7 @@ export class Replayer { imageMap: this.imageMap, errorHandler: this.warnCanvasMutationFailed.bind(this), }); + break; } case IncrementalSource.Font: { @@ -1597,6 +1590,21 @@ export class Replayer { return; } + if ( + '__sn' in parent && + parent.__sn.type === NodeType.Element && + parent.__sn.tagName === 'textarea' && + mutation.node.type === NodeType.Text + ) { + // https://github.com/rrweb-io/rrweb/issues/745 + // parent is textarea, will only keep one child node as the value + for (const c of Array.from(parent.childNodes)) { + if (c.nodeType === parent.TEXT_NODE) { + parent.removeChild(c); + } + } + } + if (previous && previous.nextSibling && previous.nextSibling.parentNode) { parent.insertBefore(target, previous.nextSibling); } else if (next && next.parentNode) { @@ -1755,6 +1763,13 @@ export class Replayer { left: d.x, behavior: 'smooth', }); + } else if (target.__sn.type === NodeType.Document) { + // nest iframe content document + ((target as unknown) as Document).defaultView!.scrollTo({ + top: d.y, + left: d.x, + behavior: 'smooth', + }); } else { try { ((target as Node) as Element).scrollTop = d.y; @@ -1896,7 +1911,7 @@ export class Replayer { } private backToNormal() { - this.inactiveEndTimestamp = null; + this.nextUserInteractionEvent = null; if (this.speedService.state.matches('normal')) { return; } @@ -1982,9 +1997,13 @@ export class Replayer { private restoreNodeSheet(node: INode) { const storedRules = this.virtualStyleRulesMap.get(node); - if (node.nodeName !== 'STYLE') return; + if (node.nodeName !== 'STYLE') { + return; + } - if (!storedRules) return; + if (!storedRules) { + return; + } const styleNode = (node as unknown) as HTMLStyleElement; diff --git a/src/replay/timer.ts b/src/replay/timer.ts index 9799a2e2..1fdbdb02 100644 --- a/src/replay/timer.ts +++ b/src/replay/timer.ts @@ -122,6 +122,7 @@ export function addDelay( event.delay = firstTimestamp - baselineTime; return firstTimestamp - baselineTime; } + event.delay = event.timestamp - baselineTime; if (lastDelay) { From 70915f13b7c302467a7b62823a5456e91187718c Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 4 Jan 2022 16:37:22 -0800 Subject: [PATCH 2/4] Add missing changes --- src/record/mutation.ts | 49 +++++++-- src/record/shadow-dom-manager.ts | 2 +- src/replay/styles/style.css | 182 ++----------------------------- src/replay/virtual-styles.ts | 1 - src/snapshot/rebuild.ts | 24 +++- src/snapshot/snapshot.ts | 44 +++++++- src/utils.ts | 3 +- 7 files changed, 113 insertions(+), 192 deletions(-) diff --git a/src/record/mutation.ts b/src/record/mutation.ts index d3cf5585..925ff433 100644 --- a/src/record/mutation.ts +++ b/src/record/mutation.ts @@ -489,13 +489,47 @@ export default class MutationBuffer { break; } } - // overwrite attribute if the mutations was triggered in same time - item.attributes[m.attributeName!] = transformAttribute( - this.doc, - (m.target as HTMLElement).tagName, - m.attributeName!, - value!, - ); + if (m.attributeName === 'style') { + const old = this.doc.createElement('span'); + if (m.oldValue) { + old.setAttribute('style', m.oldValue); + } + if ( + item.attributes.style === undefined || + item.attributes.style === null + ) { + item.attributes.style = {}; + } + const styleObj = item.attributes.style as styleAttributeValue; + for (const pname of Array.from(target.style)) { + const newValue = target.style.getPropertyValue(pname); + const newPriority = target.style.getPropertyPriority(pname); + if ( + newValue !== old.style.getPropertyValue(pname) || + newPriority !== old.style.getPropertyPriority(pname) + ) { + if (newPriority === '') { + styleObj[pname] = newValue; + } else { + styleObj[pname] = [newValue, newPriority]; + } + } + } + for (const pname of Array.from(old.style)) { + if (target.style.getPropertyValue(pname) === '') { + // "if not set, returns the empty string" + styleObj[pname] = false; // delete + } + } + } else { + // overwrite attribute if the mutations was triggered in same time + item.attributes[m.attributeName!] = transformAttribute( + this.doc, + (m.target as HTMLElement).tagName, + m.attributeName!, + value!, + ); + } break; } case 'childList': { @@ -569,6 +603,7 @@ export default class MutationBuffer { this.addedSet.add(n); this.droppedSet.delete(n); } + // if this node is blocked `serializeNode` will turn it into a placeholder element // but we have to remove it's children otherwise they will be added as placeholders too if (!isBlocked(n, this.blockClass)) diff --git a/src/record/shadow-dom-manager.ts b/src/record/shadow-dom-manager.ts index 0c4e4e93..5b2b43cc 100644 --- a/src/record/shadow-dom-manager.ts +++ b/src/record/shadow-dom-manager.ts @@ -67,7 +67,7 @@ export class ShadowDomManager { this.bypassOptions.iframeManager, this, shadowRoot, - this.bypassOptions.enableStrictPrivacy + this.bypassOptions.enableStrictPrivacy, ); initScrollObserver( this.scrollCb, diff --git a/src/replay/styles/style.css b/src/replay/styles/style.css index 5b961c68..e0163a83 100644 --- a/src/replay/styles/style.css +++ b/src/replay/styles/style.css @@ -1,82 +1,3 @@ -.rr-controller.svelte-dxnc1j.svelte-dxnc1j { - width: 100%; - height: 80px; - background: #fff; - display: flex; - flex-direction: column; - justify-content: space-around; - align-items: center; - border-radius: 0 0 5px 5px; -} -.rr-timeline.svelte-dxnc1j.svelte-dxnc1j { - width: 80%; - display: flex; - align-items: center; -} -.rr-timeline__time.svelte-dxnc1j.svelte-dxnc1j { - display: inline-block; - width: 100px; - text-align: center; - color: #11103e; -} -.rr-progress.svelte-dxnc1j.svelte-dxnc1j { - flex: 1; - height: 12px; - background: #eee; - position: relative; - border-radius: 3px; - cursor: pointer; - box-sizing: border-box; - border-top: solid 4px #fff; - border-bottom: solid 4px #fff; -} -.rr-progress.disabled.svelte-dxnc1j.svelte-dxnc1j { - cursor: not-allowed; -} -.rr-progress__step.svelte-dxnc1j.svelte-dxnc1j { - height: 100%; - position: absolute; - left: 0; - top: 0; - background: #e0e1fe; -} -.rr-progress__handler.svelte-dxnc1j.svelte-dxnc1j { - width: 20px; - height: 20px; - border-radius: 10px; - position: absolute; - top: 2px; - transform: translate(-50%, -50%); - background: rgb(73, 80, 246); -} -.rr-controller__btns.svelte-dxnc1j.svelte-dxnc1j { - display: flex; - align-items: center; - justify-content: center; - font-size: 13px; -} -.rr-controller__btns.svelte-dxnc1j button.svelte-dxnc1j { - width: 32px; - height: 32px; - display: flex; - padding: 0; - align-items: center; - justify-content: center; - background: none; - border: none; - border-radius: 50%; - cursor: pointer; -} -.rr-controller__btns.svelte-dxnc1j button.svelte-dxnc1j:active { - background: #e0e1fe; -} -.rr-controller__btns.svelte-dxnc1j button.active.svelte-dxnc1j { - color: #fff; - background: rgb(73, 80, 246); -} -.rr-controller__btns.svelte-dxnc1j button.svelte-dxnc1j:disabled { - cursor: not-allowed; -} .replayer-wrapper { position: relative; } @@ -88,7 +9,7 @@ background-size: contain; background-position: center center; background-repeat: no-repeat; - background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMi4wLjEsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgdmlld0JveD0iMCAwIDMwMCAzMDAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwMCAzMDA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+DQoJLnN0MHtmaWxsOiNGRkZGRkY7fQ0KCS5zdDF7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjg7c3Ryb2tlLW1pdGVybGltaXQ6MTA7fQ0KPC9zdHlsZT4NCjxnIGlkPSJMYXllcl8yIj4NCgk8cG9seWdvbiBjbGFzcz0ic3QwIiBwb2ludHM9IjE0LDE3IDk4LjMsMjY0IDE3NCwxOTIgMjYxLjcsMjc5LjUgMjc5LjUsMjYxLjcgMTkyLDE3NCAyNjQsOTguMyAJIi8+DQo8L2c+DQo8ZyBpZD0iTGF5ZXJfMSI+DQoJPHRpdGxlPkRlc2lnbl90bnA8L3RpdGxlPg0KCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0yOTIuMywyNTcuNWwtODcuOC04Ny43TDI2NiwxMDhjMi4zLTIuNCwyLjItNi4yLTAuMi04LjVjLTAuNS0wLjUtMS4xLTAuOS0xLjgtMS4yTDE0LjEsNi40DQoJCUMxMSw1LjIsNy41LDYuNyw2LjQsOS44Yy0wLjUsMS40LTAuNSwyLjksMCw0LjNsOTIsMjQ5LjljMS4xLDMuMSw0LjYsNC43LDcuNywzLjZjMC44LTAuMywxLjYtMC44LDIuMi0xLjRsNjEuNS02MS43bDg3LjcsODcuOA0KCQljMi4zLDIuMyw2LjEsMi4zLDguNSwwbDI2LjMtMjYuM0MyOTQuNiwyNjMuNywyOTQuNiwyNTkuOSwyOTIuMywyNTcuNUMyOTIuMywyNTcuNSwyOTIuMywyNTcuNSwyOTIuMywyNTcuNXogTTI2MS43LDI3OS41TDE3NCwxOTINCgkJYy0yLjMtMi4zLTYuMS0yLjMtOC41LDBsLTU5LjEsNTkuMWwtODQuMy0yMjlsMjI4LjcsODRMMTkyLDE2NS41Yy0yLjQsMi4zLTIuNCw2LjEsMCw4LjVjMCwwLDAsMCwwLDBsODcuNSw4Ny43TDI2MS43LDI3OS41eiIvPg0KPC9nPg0KPC9zdmc+DQo='); + background-image: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDUwIDUwIiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPkRlc2lnbl90bnA8L3RpdGxlPjxwYXRoIGQ9Ik00OC43MSw0Mi45MUwzNC4wOCwyOC4yOSw0NC4zMywxOEExLDEsMCwwLDAsNDQsMTYuMzlMMi4zNSwxLjA2QTEsMSwwLDAsMCwxLjA2LDIuMzVMMTYuMzksNDRhMSwxLDAsMCwwLDEuNjUuMzZMMjguMjksMzQuMDgsNDIuOTEsNDguNzFhMSwxLDAsMCwwLDEuNDEsMGw0LjM4LTQuMzhBMSwxLDAsMCwwLDQ4LjcxLDQyLjkxWm0tNS4wOSwzLjY3TDI5LDMyYTEsMSwwLDAsMC0xLjQxLDBsLTkuODUsOS44NUwzLjY5LDMuNjlsMzguMTIsMTRMMzIsMjcuNThBMSwxLDAsMCwwLDMyLDI5TDQ2LjU5LDQzLjYyWiI+PC9wYXRoPjwvc3ZnPg=='); border-color: transparent; /* otherwise we transition from black when .touch-device class is added */ } .replayer-mouse::after { @@ -96,7 +17,6 @@ display: inline-block; width: 20px; height: 20px; - border-radius: 10px; background: rgb(73, 80, 246); border-radius: 100%; transform: translate(-50%, -50%); @@ -107,23 +27,19 @@ } .replayer-mouse.touch-device { background-image: none; /* there's no passive cursor on touch-only screens */ - width: 25px; - height: 25px; - border-width: 2px; + width: 70px; + height: 70px; + border-width: 4px; border-style: solid; border-radius: 100%; margin-left: -37px; margin-top: -37px; - border-color: rgba(255, 255, 255, 0%); - background-color: rgba(0, 0, 0, 0%); - transition: left 0s linear, top 0s linear, border-color 0.2s ease-in-out, - background-color 0.2s ease-in-out; + border-color: rgba(73, 80, 246, 0); + transition: left 0s linear, top 0s linear, border-color 0.2s ease-in-out; } .replayer-mouse.touch-device.touch-active { - border-color: rgba(255, 255, 255, 25%); - background-color: rgba(0, 0, 0, 45%); - transition: left 0.25s linear, top 0.25s linear, border-color 0.2s ease-in-out, - background-color 0.2s ease-in-out; + border-color: rgba(73, 80, 246, 1); + transition: left 0.25s linear, top 0.25s linear, border-color 0.2s ease-in-out; } .replayer-mouse.touch-device::after { opacity: 0; /* there's no passive cursor on touch-only screens */ @@ -135,6 +51,7 @@ position: absolute; pointer-events: none; } + @keyframes click { 0% { opacity: 0.3; @@ -147,6 +64,7 @@ height: 10px; } } + @keyframes touch-click { 0% { opacity: 0; @@ -159,83 +77,3 @@ height: 10px; } } - -.rr-player { - position: relative; - background: white; - float: left; - border-radius: 5px; - box-shadow: 0 24px 48px rgba(17, 16, 62, 0.12); -} -.rr-player__frame { - overflow: hidden; -} -.replayer-wrapper { - float: left; - clear: both; - transform-origin: top left; - left: 50%; - top: 50%; -} -.replayer-wrapper > iframe { - border: none; -} -.switch.svelte-1mmdovf.svelte-1mmdovf { - height: 1em; - display: flex; - align-items: center; -} -.switch.disabled.svelte-1mmdovf.svelte-1mmdovf { - opacity: 0.5; -} -.label.svelte-1mmdovf.svelte-1mmdovf { - margin: 0 8px; -} -.switch.svelte-1mmdovf input[type='checkbox'].svelte-1mmdovf { - position: absolute; - opacity: 0; -} -.switch.svelte-1mmdovf label.svelte-1mmdovf { - width: 2em; - height: 1em; - position: relative; - cursor: pointer; - display: block; -} -.switch.disabled.svelte-1mmdovf label.svelte-1mmdovf { - cursor: not-allowed; -} -.switch.svelte-1mmdovf label.svelte-1mmdovf:before { - content: ''; - position: absolute; - width: 2em; - height: 1em; - left: 0.1em; - transition: background 0.1s ease; - background: rgba(73, 80, 246, 0.5); - border-radius: 50px; -} -.switch.svelte-1mmdovf label.svelte-1mmdovf:after { - content: ''; - position: absolute; - width: 1em; - height: 1em; - border-radius: 50px; - left: 0; - transition: all 0.2s ease; - box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.3); - background: #fcfff4; - animation: switch-off 0.2s ease-out; - z-index: 2; -} -.switch - input[type='checkbox']:checked - + label.svelte-1mmdovf.svelte-1mmdovf:before { - background: rgb(73, 80, 246); -} -.switch - input[type='checkbox']:checked - + label.svelte-1mmdovf.svelte-1mmdovf:after { - animation: switch-on 0.2s ease-out; - left: 1.1em; -} diff --git a/src/replay/virtual-styles.ts b/src/replay/virtual-styles.ts index 5e0ee281..010d90ae 100644 --- a/src/replay/virtual-styles.ts +++ b/src/replay/virtual-styles.ts @@ -21,7 +21,6 @@ type SnapshotRule = { type: StyleRuleType.Snapshot; cssTexts: string[]; }; - type SetPropertyRule = { type: StyleRuleType.SetProperty; index: number[]; diff --git a/src/snapshot/rebuild.ts b/src/snapshot/rebuild.ts index 331bbb8a..74a1e4d8 100644 --- a/src/snapshot/rebuild.ts +++ b/src/snapshot/rebuild.ts @@ -59,8 +59,8 @@ function getTagName(n: elementNode): string { } // based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping -function escapeRegExp(string: string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +function escapeRegExp(str: string) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } const HOVER_SELECTOR = /([^\\]):hover/; @@ -71,6 +71,7 @@ export function addHoverClass(cssText: string, cache: BuildCache): string { } const cachedStyle = cache?.stylesWithHoverClass.get(cssText); if (cachedStyle) return cachedStyle; + const ast = parse(cssText, { silent: true, }); @@ -90,7 +91,9 @@ export function addHoverClass(cssText: string, cache: BuildCache): string { } }); - if (selectors.length === 0) return cssText; + if (selectors.length === 0) { + return cssText; + } const selectorMatcher = new RegExp( selectors @@ -190,12 +193,25 @@ function buildNode( } else if ( tagName === 'meta' && n.attributes['http-equiv'] === 'Content-Security-Policy' && - name == 'content' + name === 'content' ) { // If CSP contains style-src and inline-style is disabled, there will be an error "Refused to apply inline style because it violates the following Content Security Policy directive: style-src '*'". // And the function insertStyleRules in rrweb replayer will throw an error "Uncaught TypeError: Cannot read property 'insertRule' of null". node.setAttribute('csp-content', value); continue; + } else if ( + tagName === 'link' && + n.attributes.rel === 'preload' && + n.attributes.as === 'script' + ) { + // ignore + } else if ( + tagName === 'link' && + n.attributes.rel === 'prefetch' && + typeof n.attributes.href === 'string' && + n.attributes.href.endsWith('.js') + ) { + // ignore } else { node.setAttribute(name, value); } diff --git a/src/snapshot/snapshot.ts b/src/snapshot/snapshot.ts index 945bda85..58359440 100644 --- a/src/snapshot/snapshot.ts +++ b/src/snapshot/snapshot.ts @@ -54,7 +54,9 @@ function getCssRuleString(rule: CSSRule): string { if (isCSSImportRule(rule)) { try { cssStringified = getCssRulesString(rule.styleSheet) || cssStringified; - } catch {} + } catch { + // ignore + } } return cssStringified; } @@ -74,7 +76,7 @@ function extractOrigin(url: string): string { return origin; } -const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")([^"]*)"|([^)]*))\)/gm; +const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm; const RELATIVE_PATH = /^(?!www\.|(?:http|ftp)s?:\/\/|[A-Za-z]:\\|\/\/|#).*/; const DATA_URI = /^(data:)([^,]*),(.*)/i; export function absoluteToStylesheet( @@ -135,8 +137,8 @@ function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { let pos = 0; function collectCharacters(regEx: RegExp) { - var chars, - match = regEx.exec(attributeValue.substring(pos)); + let chars: string; + let match = regEx.exec(attributeValue.substring(pos)); if (match) { chars = match[0]; pos += chars.length; @@ -218,7 +220,10 @@ export function transformAttribute( value: string, ): string { // relative path in attribute - if (name === 'src' || ((name === 'href' || name === 'xlink:href') && value)) { + if (name === 'src' || (name === 'href' && value)) { + return absoluteToDoc(doc, value); + } else if (name === 'xlink:href' && value && value[0] !== '#') { + // xlink:href starts with # is an id pointer return absoluteToDoc(doc, value); } else if ( name === 'background' && @@ -230,6 +235,8 @@ export function transformAttribute( return getAbsoluteSrcsetString(doc, value); } else if (name === 'style' && value) { return absoluteToStylesheet(value, getHref()); + } else if (tagName === 'object' && name === 'data' && value) { + return absoluteToDoc(doc, value); } else { return value; } @@ -343,6 +350,14 @@ function onceIframeLoaded( iframeEl.addEventListener('load', listener); } +function stringifyStyleSheet(sheet: CSSStyleSheet): string { + return sheet.cssRules + ? Array.from(sheet.cssRules) + .map((rule) => rule.cssText || '') + .join('') + : ''; +} + function serializeNode( n: Node, options: { @@ -560,6 +575,16 @@ function serializeNode( /** Determines if this node has been handled already. */ let textContentHandled = false; if (isStyle && textContent) { + try { + // try to read style sheet + if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) { + textContent = stringifyStyleSheet( + (n.parentNode as HTMLStyleElement).sheet!, + ); + } + } catch { + // ignore error + } textContent = absoluteToStylesheet(textContent, getHref()); textContentHandled = true; } @@ -646,10 +671,17 @@ function slimDOMExcluded( } else if (sn.type === NodeType.Element) { if ( slimDOMOptions.script && + // script tag (sn.tagName === 'script' || + // preload link (sn.tagName === 'link' && sn.attributes.rel === 'preload' && - sn.attributes.as === 'script')) + sn.attributes.as === 'script') || + // prefetch link + (sn.tagName === 'link' && + sn.attributes.rel === 'prefetch' && + typeof sn.attributes.href === 'string' && + sn.attributes.href.endsWith('.js'))) ) { return true; } else if ( diff --git a/src/utils.ts b/src/utils.ts index b481500e..c1f91452 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,6 +15,7 @@ import { scrollData, inputData, DocumentDimension, + IWindow, } from './types'; import { INode, @@ -27,7 +28,7 @@ import { export function on( type: string, fn: EventListenerOrEventListenerObject, - target: Document | Window = document, + target: Document | IWindow = document, ): listenerHandler { const options = { capture: true, passive: true }; target.addEventListener(type, fn, options); From 3f87814a66830951eb6e14aa352654e4970c3793 Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 4 Jan 2022 16:37:33 -0800 Subject: [PATCH 3/4] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2ff17f34..7671f304 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@highlight-run/rrweb", - "version": "1.1.0", + "version": "1.1.1", "description": "record and replay the web", "scripts": { "test": "npm run bundle:browser && cross-env TS_NODE_CACHE=false TS_NODE_FILES=true mocha -r ts-node/register -r ignore-styles -r jsdom-global/register test/**.test.ts", From 55cb96a7abec7abaf58d07f5fac35906453ef3b0 Mon Sep 17 00:00:00 2001 From: John Pham Date: Tue, 4 Jan 2022 16:38:18 -0800 Subject: [PATCH 4/4] Add missing import --- src/replay/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/replay/index.ts b/src/replay/index.ts index 67768218..34223ffa 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -40,6 +40,7 @@ import { styleAttributeValue, styleValueWithPriority, CanvasContext, + IWindow, } from '../types'; import { createMirror,