Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 38 additions & 21 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,48 @@

const untaintedBasePrototype: Partial<BasePrototypeCache> = {};

/*
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<string, TypeofPrototypeOwner>;

/*
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<T extends keyof BasePrototypeCache>(
key: T,
): BasePrototypeCache[T] {
if (untaintedBasePrototype[key])
return untaintedBasePrototype[key] as BasePrototypeCache[T];

const defaultObj = globalThis[key] as TypeofPrototypeOwner;
const defaultPrototype = defaultObj.prototype as BasePrototypeCache[T];
const candidate =
angularZoneUnpatchedAlternative(key) ||
(globalThis[key] as TypeofPrototypeOwner);
const defaultPrototype = candidate.prototype as BasePrototypeCache[T];

// use list of testable accessors to check if the prototype is tainted
const accessorNames =
Expand Down Expand Up @@ -80,16 +97,16 @@
),
);

if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent()) {
untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T];
return defaultObj.prototype as BasePrototypeCache[T];
if (isUntaintedAccessors && isUntaintedMethods) {
untaintedBasePrototype[key] = candidate.prototype as BasePrototypeCache[T];
return candidate.prototype as BasePrototypeCache[T];
}

try {
const iframeEl = document.createElement('iframe');
document.body.appendChild(iframeEl);
const win = iframeEl.contentWindow;
if (!win) return defaultObj.prototype as BasePrototypeCache[T];
if (!win) return candidate.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]
Expand Down Expand Up @@ -224,7 +241,7 @@

// copy from https://github.com/getsentry/sentry-javascript/blob/b2109071975af8bf0316d3b5b38f519bdaf5dc15/packages/utils/src/object.ts
export function patch(
source: { [key: string]: any },

Check warning on line 244 in packages/utils/src/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Unexpected any. Specify a different type
name: string,
replacement: (...args: unknown[]) => unknown,
): () => void {
Expand Down
Loading