Skip to content

Commit eaec896

Browse files
authored
fix: much better zone detection (#24)
1 parent 9eb39e4 commit eaec896

File tree

1 file changed

+38
-21
lines changed

1 file changed

+38
-21
lines changed

packages/utils/src/index.ts

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,48 @@ const testableMethods = {
2828

2929
const untaintedBasePrototype: Partial<BasePrototypeCache> = {};
3030

31-
/*
32-
When angular patches things - particularly the MutationObserver -
33-
they pass the `isNativeFunction` check
34-
That then causes performance issues
35-
because Angular's change detection
36-
doesn't like sharing a mutation observer
37-
Checking for the presence of the Zone object
38-
on global is a good-enough proxy for Angular
39-
to cover most cases
40-
(you can configure zone.js to have a different name
41-
on the global object and should then manually run rrweb
42-
outside the Zone)
43-
*/
44-
export const isAngularZonePresent = (): boolean => {
45-
return !!(globalThis as { Zone?: unknown }).Zone;
31+
type WindowWithZone = typeof globalThis & {
32+
Zone?: {
33+
__symbol__?: (key: string) => string;
34+
};
4635
};
4736

37+
type WindowWithUnpatchedSymbols = typeof globalThis &
38+
Record<string, TypeofPrototypeOwner>;
39+
40+
/*
41+
Angular zone patches many things and can pass the untainted checks below, causing performance issues
42+
Angular zone, puts the unpatched originals on the window, and the names for hose on the zone object.
43+
So, we get the unpatched versions from the window object if they exist.
44+
You can rename Zone, but this is a good enough proxy to avoid going to an iframe to get the untainted versions.
45+
see: https://github.com/angular/angular/issues/26948
46+
*/
47+
function angularZoneUnpatchedAlternative(key: keyof BasePrototypeCache) {
48+
const angularUnpatchedVersionSymbol = (
49+
globalThis as WindowWithZone
50+
)?.Zone?.__symbol__?.(key);
51+
if (
52+
angularUnpatchedVersionSymbol &&
53+
(globalThis as WindowWithUnpatchedSymbols)[angularUnpatchedVersionSymbol]
54+
) {
55+
return (globalThis as WindowWithUnpatchedSymbols)[
56+
angularUnpatchedVersionSymbol
57+
];
58+
} else {
59+
return undefined;
60+
}
61+
}
62+
4863
export function getUntaintedPrototype<T extends keyof BasePrototypeCache>(
4964
key: T,
5065
): BasePrototypeCache[T] {
5166
if (untaintedBasePrototype[key])
5267
return untaintedBasePrototype[key] as BasePrototypeCache[T];
5368

54-
const defaultObj = globalThis[key] as TypeofPrototypeOwner;
55-
const defaultPrototype = defaultObj.prototype as BasePrototypeCache[T];
69+
const candidate =
70+
angularZoneUnpatchedAlternative(key) ||
71+
(globalThis[key] as TypeofPrototypeOwner);
72+
const defaultPrototype = candidate.prototype as BasePrototypeCache[T];
5673

5774
// use list of testable accessors to check if the prototype is tainted
5875
const accessorNames =
@@ -80,16 +97,16 @@ export function getUntaintedPrototype<T extends keyof BasePrototypeCache>(
8097
),
8198
);
8299

83-
if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent()) {
84-
untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T];
85-
return defaultObj.prototype as BasePrototypeCache[T];
100+
if (isUntaintedAccessors && isUntaintedMethods) {
101+
untaintedBasePrototype[key] = candidate.prototype as BasePrototypeCache[T];
102+
return candidate.prototype as BasePrototypeCache[T];
86103
}
87104

88105
try {
89106
const iframeEl = document.createElement('iframe');
90107
document.body.appendChild(iframeEl);
91108
const win = iframeEl.contentWindow;
92-
if (!win) return defaultObj.prototype as BasePrototypeCache[T];
109+
if (!win) return candidate.prototype as BasePrototypeCache[T];
93110

94111
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
95112
const untaintedObject = (win as any)[key]

0 commit comments

Comments
 (0)