Skip to content
This repository was archived by the owner on May 7, 2026. It is now read-only.
Draft
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
60 changes: 60 additions & 0 deletions packages/rrweb/src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,47 @@ function initStyleSheetObserver(
};
}

// Queue for insertRule calls that arrive before the MutationObserver has
// added the ownerNode to the mirror (race condition with CSS-in-JS libraries
// like Emotion that call insertRule synchronously after creating <style>).
const pendingInsertRules = new Map<
Node,
Array<{ rule: string; index: number | undefined }>
>();
let pendingRetryTimer: ReturnType<typeof setTimeout> | null = null;

function schedulePendingRetry() {
if (pendingRetryTimer !== null) return;
pendingRetryTimer = setTimeout(() => {
pendingRetryTimer = null;
flushPendingRules();
}, 0);
}

function flushPendingRules() {
pendingInsertRules.forEach((adds, ownerNode) => {
const id = mirror.getId(ownerNode);
if (id === -1) {
// Node never made it into the mirror (e.g. removed before MO fired)
return;
}
const meta = mirror.getMeta(ownerNode);
if (
meta &&
'attributes' in meta &&
(meta as { attributes?: { _cssText?: string } }).attributes?._cssText
) {
// MutationObserver already serialized the full sheet as _cssText
return;
}
styleSheetRuleCb({
id,
adds: adds.map(({ rule, index }) => ({ rule, index })),
});
});
pendingInsertRules.clear();
}

// eslint-disable-next-line @typescript-eslint/unbound-method
const insertRule = win.CSSStyleSheet.prototype.insertRule;
win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, {
Expand All @@ -621,6 +662,20 @@ function initStyleSheetObserver(
styleId,
adds: [{ rule, index }],
});
} else if (
thisArg.ownerNode &&
(thisArg.ownerNode as Element).isConnected
) {
// Node not yet in mirror but attached to DOM — queue for retry
// after MutationObserver has had a chance to process it.
const ownerNode = thisArg.ownerNode as Node;
let queued = pendingInsertRules.get(ownerNode);
if (!queued) {
queued = [];
pendingInsertRules.set(ownerNode, queued);
}
queued.push({ rule, index });
schedulePendingRetry();
}
return target.apply(thisArg, argumentsList);
},
Expand Down Expand Up @@ -856,6 +911,11 @@ function initStyleSheetObserver(
type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule;
type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule;
});
if (pendingRetryTimer !== null) {
clearTimeout(pendingRetryTimer);
pendingRetryTimer = null;
}
pendingInsertRules.clear();
});
}

Expand Down