From 5c561be76d3a6fa85409c17d4cfce6c39ca5b706 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 6 Dec 2024 07:22:56 +0000 Subject: [PATCH] And test on a mobile device too --- .github/workflows/testcafe.yml | 3 +- .../e2e/session-recording.fake-angular.cy.ts | 63 + package.json | 3 +- patches/@rrweb__record@2.0.0-alpha.17.patch | 52 +- playground/cypress/index.html | 5 + pnpm-lock.yaml | 12658 +++++++++------- react/pnpm-lock.yaml | 5000 +++--- 7 files changed, 9868 insertions(+), 7916 deletions(-) create mode 100644 cypress/e2e/session-recording.fake-angular.cy.ts diff --git a/.github/workflows/testcafe.yml b/.github/workflows/testcafe.yml index 7fffb7e3f..8e303a6f5 100644 --- a/.github/workflows/testcafe.yml +++ b/.github/workflows/testcafe.yml @@ -24,6 +24,7 @@ jobs: - 'firefox:headless' - 'browserstack:ie' - 'browserstack:safari' + - 'browserstack:safari:emulation:device=iPhone 4' include: - browser: 'chrome:headless' name: Chrome @@ -31,7 +32,7 @@ jobs: name: Firefox - browser: 'browserstack:ie' name: IE11 - - browser: 'browserstack:safari' + - browser: 'browserstack:safari:emulation:device=iPhone 4' name: Safari steps: diff --git a/cypress/e2e/session-recording.fake-angular.cy.ts b/cypress/e2e/session-recording.fake-angular.cy.ts new file mode 100644 index 000000000..9c359b6cc --- /dev/null +++ b/cypress/e2e/session-recording.fake-angular.cy.ts @@ -0,0 +1,63 @@ +/// + +import { start } from '../support/setup' + +/** + * We have seen that when Angular "taints" prototypes and rrweb loads fresh copies from an iframe + * That iOS and Safari were not providing a mutation observer, this created unplayable recordings + * let's assert that we do get mutations + */ +describe('Session recording', () => { + describe('with fake angular running', () => { + beforeEach(() => { + cy.window().then((win) => { + ;(win as any).Zone = { my: 'fake zone' } + }) + }) + + it('captures session events despite getting untainted things from iframe', () => { + start({ + options: { + session_recording: {}, + }, + decideResponseOverrides: { + isAuthenticated: false, + sessionRecording: { + endpoint: '/ses/', + }, + capturePerformance: true, + autocapture_opt_out: true, + }, + url: './playground/cypress', + }) + cy.wait('@recorder-script') + + cy.get('[data-cy-change-dom-button]') + .click() + .wait('@session-recording') + .then(() => { + cy.phCaptures({ full: true }).then((captures) => { + expect(captures.map((c) => c.event)).to.deep.equal(['$pageview', '$snapshot']) + + expect(captures[1]['properties']['$snapshot_data']).to.have.length.above(9).and.below(20) + // a meta and then a full snapshot + expect(captures[1]['properties']['$snapshot_data'][0].type).to.equal(4) // meta + expect(captures[1]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot + expect(captures[1]['properties']['$snapshot_data'][2].type).to.equal(5) // custom event with options + expect(captures[1]['properties']['$snapshot_data'][3].type).to.equal(5) // custom event with posthog config + // Making a set from the rest should all be 3 - incremental snapshots + const incrementalSnapshots = captures[1]['properties']['$snapshot_data'].slice(4) + expect(Array.from(new Set(incrementalSnapshots.map((s) => s.type)))).to.deep.eq([3]) + + const mutations = incrementalSnapshots.filter((s) => !!s.data && s.data.source === 0) + expect(mutations).to.have.length(1) + + const { attributes, removes, adds } = mutations[0].data + expect(attributes[0].attributes.style).to.eql({ 'background-color': 'blue' }) + expect(removes).to.have.length(1) + expect(adds[0].node.textContent).to.eq('i r been changed') + }) + }) + }) + }) +}) diff --git a/package.json b/package.json index 4a42b0052..02fbf0354 100644 --- a/package.json +++ b/package.json @@ -130,5 +130,6 @@ "@rrweb/rrweb-plugin-console-record@2.0.0-alpha.17": "patches/@rrweb__rrweb-plugin-console-record@2.0.0-alpha.17.patch", "@rrweb/record@2.0.0-alpha.17": "patches/@rrweb__record@2.0.0-alpha.17.patch" } - } + }, + "packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab" } diff --git a/patches/@rrweb__record@2.0.0-alpha.17.patch b/patches/@rrweb__record@2.0.0-alpha.17.patch index 0d54d7bff..c7b84bd30 100644 --- a/patches/@rrweb__record@2.0.0-alpha.17.patch +++ b/patches/@rrweb__record@2.0.0-alpha.17.patch @@ -1,5 +1,5 @@ diff --git a/dist/record.js b/dist/record.js -index 46ec389fefb698243008b39db65470dbdf0a3857..a18724d8b6ba43a30935daf257127fbb0c898541 100644 +index 46ec389fefb698243008b39db65470dbdf0a3857..6396cbb3f6b41d7b58920bac1499e4695f844365 100644 --- a/dist/record.js +++ b/dist/record.js @@ -26,6 +26,14 @@ const testableMethods$1 = { @@ -17,29 +17,47 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..a18724d8b6ba43a30935daf257127fbb const untaintedBasePrototype$1 = {}; function getUntaintedPrototype$1(key) { if (untaintedBasePrototype$1[key]) -@@ -54,7 +62,7 @@ function getUntaintedPrototype$1(key) { +@@ -54,22 +62,30 @@ function getUntaintedPrototype$1(key) { } ) ); - if (isUntaintedAccessors && isUntaintedMethods) { +- untaintedBasePrototype$1[key] = defaultObj.prototype; +- return defaultObj.prototype; ++ let valueToUse = defaultObj.prototype; ++ + if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent()) { - untaintedBasePrototype$1[key] = defaultObj.prototype; - return defaultObj.prototype; ++ untaintedBasePrototype[key] = defaultObj.prototype; ++ return valueToUse; } -@@ -65,10 +73,10 @@ function getUntaintedPrototype$1(key) { - if (!win) return defaultObj.prototype; - const untaintedObject = win[key].prototype; ++ + try { +- const iframeEl = document.createElement("iframe"); ++ const iframeEl = document.createElement('iframe'); ++ iframeEl.hidden = true; + document.body.appendChild(iframeEl); + const win = iframeEl.contentWindow; +- if (!win) return defaultObj.prototype; +- const untaintedObject = win[key].prototype; ++ const wink = (win||{})[key] ++ const prototype = wink?.prototype ++ if (prototype) { ++ valueToUse = prototype; ++ } ++ ++ // cleanup document.body.removeChild(iframeEl); - if (!untaintedObject) return defaultPrototype; -+ if (!untaintedObject) return defaultObj.prototype; - return untaintedBasePrototype$1[key] = untaintedObject; +- return untaintedBasePrototype$1[key] = untaintedObject; } catch { - return defaultPrototype; -+ return defaultObj.prototype; ++ // ignore } ++ return valueToUse } const untaintedAccessorCache$1 = {}; -@@ -246,6 +254,9 @@ function isCSSImportRule(rule2) { + function getUntaintedAccessor$1(key, instance, accessor) { +@@ -246,6 +262,9 @@ function isCSSImportRule(rule2) { function isCSSStyleRule(rule2) { return "selectorText" in rule2; } @@ -49,7 +67,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..a18724d8b6ba43a30935daf257127fbb class Mirror { constructor() { __publicField$1(this, "idNodeMap", /* @__PURE__ */ new Map()); -@@ -809,9 +820,14 @@ function serializeElementNode(n2, options) { +@@ -809,9 +828,14 @@ function serializeElementNode(n2, options) { } } if (tagName === "link" && inlineStylesheet) { @@ -67,7 +85,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..a18724d8b6ba43a30935daf257127fbb let cssText = null; if (stylesheet) { cssText = stringifyStylesheet(stylesheet); -@@ -855,7 +871,15 @@ function serializeElementNode(n2, options) { +@@ -855,7 +879,15 @@ function serializeElementNode(n2, options) { } } if (tagName === "dialog" && n2.open) { @@ -84,7 +102,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..a18724d8b6ba43a30935daf257127fbb } if (tagName === "canvas" && recordCanvas) { if (n2.__context === "2d") { -@@ -1116,7300 +1140,227 @@ function serializeNodeWithId(n2, options) { +@@ -1116,7300 +1148,227 @@ function serializeNodeWithId(n2, options) { keepIframeSrcFn }; if (serializedNode.type === NodeType$2.Element && serializedNode.tagName === "textarea" && serializedNode.attributes.value !== void 0) ; @@ -7594,7 +7612,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..a18724d8b6ba43a30935daf257127fbb class BaseRRNode { // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any constructor(..._args) { -@@ -8507,7 +1458,7 @@ function getUntaintedPrototype(key) { +@@ -8507,7 +1466,7 @@ function getUntaintedPrototype(key) { } ) ); @@ -7603,7 +7621,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..a18724d8b6ba43a30935daf257127fbb untaintedBasePrototype[key] = defaultObj.prototype; return defaultObj.prototype; } -@@ -11382,11 +4333,19 @@ class CanvasManager { +@@ -11382,11 +4341,19 @@ class CanvasManager { let rafId; const getCanvas = () => { const matchedCanvas = []; @@ -7628,7 +7646,7 @@ index 46ec389fefb698243008b39db65470dbdf0a3857..a18724d8b6ba43a30935daf257127fbb return matchedCanvas; }; const takeCanvasSnapshots = (timestamp) => { -@@ -11407,13 +4366,20 @@ class CanvasManager { +@@ -11407,13 +4374,20 @@ class CanvasManager { context.clear(context.COLOR_BUFFER_BIT); } } diff --git a/playground/cypress/index.html b/playground/cypress/index.html index b078af68e..0f99e670e 100644 --- a/playground/cypress/index.html +++ b/playground/cypress/index.html @@ -23,6 +23,11 @@ Make XHR post network call +
I START RED, BUT CAN BE CHANGED BY A BUTTON BELOW
+ +