diff --git a/.changeset/three-gifts-clap.md b/.changeset/three-gifts-clap.md new file mode 100644 index 000000000..81da64653 --- /dev/null +++ b/.changeset/three-gifts-clap.md @@ -0,0 +1,14 @@ +--- +'@web/browser-logs': patch +--- + +Avoid browser crashes when serializing browser logs of complex objects + +If we are seeing too many object iterations, we know something is off. This can +happen when we are seeing e.g. large linked lists where every element recursively +has access to the full list again. This will not quickly yield circular early bail-outs, +but instead result in the stringification to take LOTS of processing time and results +in browser crashes. We should limit object iterations for this reason, and instead expect +people to inspect such logs in the browser directly. + +Related #2798. diff --git a/packages/browser-logs/src/serialize.ts b/packages/browser-logs/src/serialize.ts index 0a458468e..4b3cdc34d 100644 --- a/packages/browser-logs/src/serialize.ts +++ b/packages/browser-logs/src/serialize.ts @@ -71,6 +71,7 @@ function serializeObject(value: any) { function createReplacer() { // maintain a stack of seen objects to handle circular references var objectStack: any[] = []; + var objectIterations = 0; return function replacer(this: any, key: string, value: unknown) { if (this[KEY_WTR_TYPE]) { @@ -113,6 +114,17 @@ function createReplacer() { } objectStack.unshift(value); + // If we are seeing too many object iterations, we know something is off. This can + // happen when we are seeing e.g. large linked lists where every element recursively + // has access to the full list again. This will not quickly yield circular early bail-outs, + // but instead result in the stringification to take LOTS of processing time and results + // in browser crashes. We should limit object iterations for this reason, and instead expect + // people to inspect such logs in the browser directly. + objectIterations++; + if (objectIterations > 10_000) { + return '[Serialization Limit]'; + } + if (Array.isArray(value)) { return value; } diff --git a/packages/browser-logs/test/serialize-deserialize.test.ts b/packages/browser-logs/test/serialize-deserialize.test.ts index 5a3232684..eb949d50a 100644 --- a/packages/browser-logs/test/serialize-deserialize.test.ts +++ b/packages/browser-logs/test/serialize-deserialize.test.ts @@ -336,6 +336,46 @@ describe('serialize deserialize', function () { expect(deserialized.x).to.equal('[Circular]'); }); + it('handles complex circular references', async () => { + const serialized = await page.evaluate(() => { + interface ChipNode { + idx: number; + next?: ChipNode; + prev?: ChipNode; + val: unknown; + } + class MatChip { + viewRef: { nodes: ChipNode[] } = { nodes: [] }; + + constructor() { + let prevNode: ChipNode | null = null; + for (let i = 0; i < 1000; i++) { + const newNode: ChipNode = { + idx: i, + val: this, + next: this.viewRef.nodes[0], + }; + + this.viewRef.nodes.push(newNode); + + if (prevNode) { + prevNode.next = newNode; + newNode.prev = prevNode; + } + + prevNode = newNode; + } + } + } + return (window as any)._serialize(new MatChip()); + }); + + await deserialize(serialized); + + // No assertion. Simply expect this to pass and not crash. + expect(true).to.eq(true); + }); + it('handles errors', async () => { const serialized = await page.evaluate(() => { const c = () => new Error('my error msg');