From 825b0bace6387270df758057fbb1681259142b95 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 19 Nov 2024 15:35:54 +0000 Subject: [PATCH 1/7] fix: angular wrapped mutationobserver detection --- packages/utils/src/index.ts | 39 +++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b88d7f452e..d8e9a01f4f 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -28,6 +28,29 @@ const testableMethods = { const untaintedBasePrototype: Partial = {}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const isFunction = (x: unknown): x is (...args: any[]) => any => { + return typeof x === 'function' +} + +function isNativeFunction(x: unknown): boolean { + return isFunction(x) && x.toString().includes("[native code]"); +} + +/* + When angular patches things they pass the above `isNativeFunction` check + That then causes performance issues + because angular's change detection + doesn't like sharing a mutation observer + */ +export const isAngularZonePatchedFunction = (x: unknown): boolean => { + if (!isFunction(x)) { + return false + } + const prototypeKeys = Object.getOwnPropertyNames(x.prototype || {}) + return prototypeKeys.some((key) => key.indexOf('__zone')) +} + export function getUntaintedPrototype( key: T, ): BasePrototypeCache[T] { @@ -43,12 +66,16 @@ export function getUntaintedPrototype( const isUntaintedAccessors = Boolean( accessorNames && // @ts-expect-error 2345 - accessorNames.every((accessor: keyof typeof defaultPrototype) => - Boolean( - Object.getOwnPropertyDescriptor(defaultPrototype, accessor) - ?.get?.toString() - .includes('[native code]'), - ), + accessorNames.every((accessor: keyof typeof defaultPrototype) => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const candidate = Object.getOwnPropertyDescriptor(defaultPrototype, accessor) + ?.get; + let isUntainted = isNativeFunction(candidate) + if (key === 'MutationObserver') { + isUntainted = isUntainted && !isAngularZonePatchedFunction(candidate) + } + return isUntainted + }, ), ); From 44c84b26ce06dcc1070e783da9b7c98975add62d Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 19 Nov 2024 15:56:06 +0000 Subject: [PATCH 2/7] add change set --- .changeset/moody-experts-build.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/moody-experts-build.md diff --git a/.changeset/moody-experts-build.md b/.changeset/moody-experts-build.md new file mode 100644 index 0000000000..ac61925adf --- /dev/null +++ b/.changeset/moody-experts-build.md @@ -0,0 +1,5 @@ +--- +"@rrweb/record": patch +--- + +correctly detect when angular has wrapped mutation observer" From 82a92fdeda01c58d46087099079820f6e840e2fa Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 19 Nov 2024 21:02:51 +0000 Subject: [PATCH 3/7] fix --- packages/utils/src/index.ts | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index d8e9a01f4f..f75b4842cc 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -33,10 +33,6 @@ const isFunction = (x: unknown): x is (...args: any[]) => any => { return typeof x === 'function' } -function isNativeFunction(x: unknown): boolean { - return isFunction(x) && x.toString().includes("[native code]"); -} - /* When angular patches things they pass the above `isNativeFunction` check That then causes performance issues @@ -66,16 +62,12 @@ export function getUntaintedPrototype( const isUntaintedAccessors = Boolean( accessorNames && // @ts-expect-error 2345 - accessorNames.every((accessor: keyof typeof defaultPrototype) => { - // eslint-disable-next-line @typescript-eslint/unbound-method - const candidate = Object.getOwnPropertyDescriptor(defaultPrototype, accessor) - ?.get; - let isUntainted = isNativeFunction(candidate) - if (key === 'MutationObserver') { - isUntainted = isUntainted && !isAngularZonePatchedFunction(candidate) - } - return isUntainted - }, + accessorNames.every((accessor: keyof typeof defaultPrototype) => + Boolean( + Object.getOwnPropertyDescriptor(defaultPrototype, accessor) + ?.get?.toString() + .includes('[native code]'), + ), ), ); @@ -90,7 +82,7 @@ export function getUntaintedPrototype( ), ); - if (isUntaintedAccessors && isUntaintedMethods) { + if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePatchedFunction(defaultObj)) { untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T]; return defaultObj.prototype as BasePrototypeCache[T]; } From 98254bfa90ffbe7a697b22944c1ad444701618b8 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Wed, 20 Nov 2024 08:08:43 +0000 Subject: [PATCH 4/7] prettier --- packages/utils/src/index.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index f75b4842cc..a72b42390a 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -30,8 +30,8 @@ const untaintedBasePrototype: Partial = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any const isFunction = (x: unknown): x is (...args: any[]) => any => { - return typeof x === 'function' -} + return typeof x === 'function'; +}; /* When angular patches things they pass the above `isNativeFunction` check @@ -40,12 +40,12 @@ const isFunction = (x: unknown): x is (...args: any[]) => any => { doesn't like sharing a mutation observer */ export const isAngularZonePatchedFunction = (x: unknown): boolean => { - if (!isFunction(x)) { - return false - } - const prototypeKeys = Object.getOwnPropertyNames(x.prototype || {}) - return prototypeKeys.some((key) => key.indexOf('__zone')) -} + if (!isFunction(x)) { + return false; + } + const prototypeKeys = Object.getOwnPropertyNames(x.prototype || {}); + return prototypeKeys.some((key) => key.indexOf('__zone')); +}; export function getUntaintedPrototype( key: T, @@ -82,7 +82,11 @@ export function getUntaintedPrototype( ), ); - if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePatchedFunction(defaultObj)) { + if ( + isUntaintedAccessors && + isUntaintedMethods && + !isAngularZonePatchedFunction(defaultObj) + ) { untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T]; return defaultObj.prototype as BasePrototypeCache[T]; } From d781a44913f1a451be0814f24be9a55e4ab7ea7c Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 29 Nov 2024 11:50:35 +0000 Subject: [PATCH 5/7] following posthog prod --- packages/utils/src/index.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index a72b42390a..ed18987e16 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -28,23 +28,21 @@ const testableMethods = { const untaintedBasePrototype: Partial = {}; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isFunction = (x: unknown): x is (...args: any[]) => any => { - return typeof x === 'function'; -}; - /* - When angular patches things they pass the above `isNativeFunction` check + When angular patches things - particularly the MutationObserver - + they pass the `isNativeFunction` check That then causes performance issues - because angular's change detection + because Angular's change detection doesn't like sharing a mutation observer + Checking for the presence of the Zone object + on global is a good-enough proxy for Angular + to cover most cases + (you can configure zone.js to have a different name + on the global object and should then manually run rrweb + outside the Zone) */ -export const isAngularZonePatchedFunction = (x: unknown): boolean => { - if (!isFunction(x)) { - return false; - } - const prototypeKeys = Object.getOwnPropertyNames(x.prototype || {}); - return prototypeKeys.some((key) => key.indexOf('__zone')); +export const isAngularZonePresent = (): boolean => { + return !!(globalThis as { Zone?: unknown }).Zone; }; export function getUntaintedPrototype( @@ -85,7 +83,7 @@ export function getUntaintedPrototype( if ( isUntaintedAccessors && isUntaintedMethods && - !isAngularZonePatchedFunction(defaultObj) + !isAngularZonePresent() ) { untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T]; return defaultObj.prototype as BasePrototypeCache[T]; From 5131ca896829b6b43294b75ec4aff3b7fd4faede Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 29 Nov 2024 11:57:20 +0000 Subject: [PATCH 6/7] manually prettier --- packages/utils/src/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ed18987e16..1cd267c08f 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -80,11 +80,7 @@ export function getUntaintedPrototype( ), ); - if ( - isUntaintedAccessors && - isUntaintedMethods && - !isAngularZonePresent() - ) { + if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent()) { untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T]; return defaultObj.prototype as BasePrototypeCache[T]; } From 7336ce9cc4410100636baac6c9bb36b95b4dcbde Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 6 Dec 2024 08:41:05 +0000 Subject: [PATCH 7/7] Update .changeset/moody-experts-build.md Co-authored-by: Justin Halsall --- .changeset/moody-experts-build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/moody-experts-build.md b/.changeset/moody-experts-build.md index ac61925adf..fb2c399a10 100644 --- a/.changeset/moody-experts-build.md +++ b/.changeset/moody-experts-build.md @@ -2,4 +2,4 @@ "@rrweb/record": patch --- -correctly detect when angular has wrapped mutation observer" +Correctly detect when angular has wrapped mutation observer