diff --git a/.changeset/tall-poems-develop.md b/.changeset/tall-poems-develop.md new file mode 100644 index 0000000000..3324126c36 --- /dev/null +++ b/.changeset/tall-poems-develop.md @@ -0,0 +1,5 @@ +--- +"@rrweb/utils": patch +--- + +load unpatched versions of things from Angular zone when present diff --git a/.github/workflows/style-check.yml b/.github/workflows/style-check.yml index e47ee3c9c2..9aaf14a6e9 100644 --- a/.github/workflows/style-check.yml +++ b/.github/workflows/style-check.yml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest name: ESLint Annotation steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: eslint_report.json - name: Annotate Code Linting Results diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 1cd267c08f..258a5b5d66 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -28,30 +28,47 @@ const testableMethods = { const untaintedBasePrototype: Partial = {}; -/* - When angular patches things - particularly the MutationObserver - - they pass the `isNativeFunction` check - That then causes performance issues - 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 isAngularZonePresent = (): boolean => { - return !!(globalThis as { Zone?: unknown }).Zone; +type WindowWithZone = typeof globalThis & { + Zone?: { + __symbol__?: (key: string) => string; + }; }; +type WindowWithUnpatchedSymbols = typeof globalThis & + Record; + +/* +Angular zone patches many things and can pass the untainted checks below, causing performance issues +Angular zone, puts the unpatched originals on the window, and the names for hose on the zone object. +So, we get the unpatched versions from the window object if they exist. +You can rename Zone, but this is a good enough proxy to avoid going to an iframe to get the untainted versions. +see: https://github.com/angular/angular/issues/26948 +*/ +function angularZoneUnpatchedAlternative(key: keyof BasePrototypeCache) { + const angularUnpatchedVersionSymbol = ( + globalThis as WindowWithZone + )?.Zone?.__symbol__?.(key); + if ( + angularUnpatchedVersionSymbol && + (globalThis as WindowWithUnpatchedSymbols)[angularUnpatchedVersionSymbol] + ) { + return (globalThis as WindowWithUnpatchedSymbols)[ + angularUnpatchedVersionSymbol + ]; + } else { + return undefined; + } +} + export function getUntaintedPrototype( key: T, ): BasePrototypeCache[T] { if (untaintedBasePrototype[key]) return untaintedBasePrototype[key] as BasePrototypeCache[T]; - const defaultObj = globalThis[key] as TypeofPrototypeOwner; + const defaultObj = + angularZoneUnpatchedAlternative(key) || + (globalThis[key] as TypeofPrototypeOwner); const defaultPrototype = defaultObj.prototype as BasePrototypeCache[T]; // use list of testable accessors to check if the prototype is tainted @@ -80,29 +97,35 @@ export function getUntaintedPrototype( ), ); - if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent()) { - untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T]; - return defaultObj.prototype as BasePrototypeCache[T]; + const isUntainted = isUntaintedAccessors && isUntaintedMethods; + // we're going to default to what we do have + let impl: BasePrototypeCache[T] = + defaultObj.prototype as BasePrototypeCache[T]; + // but if it is tainted + if (!isUntainted) { + // try to load a fresh copy from a sandbox iframe + let iframeEl: HTMLIFrameElement | undefined = undefined; + try { + iframeEl = document.createElement('iframe'); + iframeEl.hidden = true; + document.body.appendChild(iframeEl); + const win = iframeEl.contentWindow; + if (win) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + const candidate = (win as any)[key].prototype as BasePrototypeCache[T]; + if (candidate) { + impl = candidate; + } + } + } finally { + if (iframeEl) { + document.body.removeChild(iframeEl); + } + } } - try { - const iframeEl = document.createElement('iframe'); - document.body.appendChild(iframeEl); - const win = iframeEl.contentWindow; - if (!win) return defaultObj.prototype as BasePrototypeCache[T]; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - const untaintedObject = (win as any)[key] - .prototype as BasePrototypeCache[T]; - // cleanup - document.body.removeChild(iframeEl); - - if (!untaintedObject) return defaultPrototype; - - return (untaintedBasePrototype[key] = untaintedObject); - } catch { - return defaultPrototype; - } + untaintedBasePrototype[key] = impl; + return impl; } const untaintedAccessorCache: Record<