Skip to content

Commit

Permalink
And test on a mobile device too
Browse files Browse the repository at this point in the history
  • Loading branch information
pauldambra committed Dec 6, 2024
1 parent eb8151f commit 5c561be
Show file tree
Hide file tree
Showing 7 changed files with 9,868 additions and 7,916 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/testcafe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ jobs:
- 'firefox:headless'
- 'browserstack:ie'
- 'browserstack:safari'
- 'browserstack:safari:emulation:device=iPhone 4'
include:
- browser: 'chrome:headless'
name: Chrome
- browser: 'firefox:headless'
name: Firefox
- browser: 'browserstack:ie'
name: IE11
- browser: 'browserstack:safari'
- browser: 'browserstack:safari:emulation:device=iPhone 4'
name: Safari

steps:
Expand Down
63 changes: 63 additions & 0 deletions cypress/e2e/session-recording.fake-angular.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/// <reference types="cypress" />

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')
})
})
})
})
})
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,6 @@
"@rrweb/[email protected]": "patches/@[email protected]",
"@rrweb/[email protected]": "patches/@[email protected]"
}
}
},
"packageManager": "[email protected]+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
}
52 changes: 35 additions & 17 deletions patches/@[email protected]
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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;
}
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) ;
Expand Down Expand Up @@ -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) {
}
)
);
Expand All @@ -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 = [];
Expand All @@ -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);
}
}
Expand Down
5 changes: 5 additions & 0 deletions playground/cypress/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
Make XHR post network call
</button>

<div id="a-red-box" style="padding: 4px 8px; color: white;width: 250px; height: 75px; background-color: red;">I START RED, BUT CAN BE CHANGED BY A BUTTON BELOW</div>
<button data-cy-change-dom-button onclick="document.getElementById('a-red-box').style.backgroundColor='blue';document.getElementById('a-red-box').innerText='i r been changed'">
Change the DOM
</button>

<script>
function makeXHRNetworkPOST() {
const xhr = new XMLHttpRequest();
Expand Down
Loading

0 comments on commit 5c561be

Please sign in to comment.