From 7cdb05185fba94db4366cbe4f35161b406be502e Mon Sep 17 00:00:00 2001 From: "Bajohr, Rayk" Date: Mon, 31 Aug 2026 21:20:46 +0200 Subject: [PATCH] feat(live-previewer): limit number of log messages --- api-goldens/live-preview/index.api.md | 1 + .../si-live-preview-renderer.component.ts | 44 ++++++++++++++++--- .../si-live-preview.component.ts | 6 ++- .../interfaces/live-preview-config.ts | 2 + 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/api-goldens/live-preview/index.api.md b/api-goldens/live-preview/index.api.md index a3aaa593f2..3b32dfe273 100644 --- a/api-goldens/live-preview/index.api.md +++ b/api-goldens/live-preview/index.api.md @@ -168,6 +168,7 @@ export interface SiLivePreviewConfig { examplesBaseUrl: string; // (undocumented) landscapeToggle?: boolean; + maxLogMessages?: number; // (undocumented) rootFontSizes?: number[]; // (undocumented) diff --git a/projects/live-preview/components/si-live-preview-renderer/si-live-preview-renderer.component.ts b/projects/live-preview/components/si-live-preview-renderer/si-live-preview-renderer.component.ts index 9992f3a94f..67bd95659b 100644 --- a/projects/live-preview/components/si-live-preview-renderer/si-live-preview-renderer.component.ts +++ b/projects/live-preview/components/si-live-preview-renderer/si-live-preview-renderer.component.ts @@ -32,16 +32,38 @@ import { LandscapeSupportService } from '../../services/landscape-support.servic import { SiDefaultLivePreviewRuntimeComponent } from './si-default-live-preview-runtime.component'; import { SiLivePreviewRuntimeComponent } from './si-live-preview-runtime.component'; -// for handling JSON.stringify with circular references, from MDN -const getCircularReplacer = (): ((_key: any, value: any | null) => any) => { - const seen = new WeakSet(); - return (_key: any, value: any | null) => { +const MAX_LOG_DEPTH = 4; +const MAX_LOG_SIZE = 50_000; + +class LogSizeExceededError extends Error {} + +/** Limit the depth and size of log messages to improve performance. */ +const getLogReplacer = (): ((this: unknown, key: string, value: unknown) => unknown) => { + const depths = new WeakMap(); + const seen = new WeakSet(); + let estimatedSize = 0; + + return function (this: unknown, key: string, value: unknown): unknown { + estimatedSize += key.length + (typeof value === 'string' ? value.length : 8); + if (estimatedSize > MAX_LOG_SIZE) { + throw new LogSizeExceededError(); + } + if (typeof value === 'object' && value !== null) { if (seen.has(value)) { - return; + return undefined; } + + const parentDepth = typeof this === 'object' && this !== null ? (depths.get(this) ?? -1) : -1; + const depth = parentDepth + 1; + if (depth > MAX_LOG_DEPTH) { + return Array.isArray(value) ? '[Array]' : '[Object]'; + } + seen.add(value); + depths.set(value, depth); } + return value; }; }; @@ -232,8 +254,16 @@ export class SiLivePreviewRendererComponent implements OnDestroy { this.createComponent(); } - private stringifyLog(args: any[]): string { - return args.map(a => JSON.stringify(a, getCircularReplacer())).join(', '); + private stringifyLog(args: unknown[]): string { + return args + .map(value => { + try { + return JSON.stringify(value, getLogReplacer()); + } catch (error) { + return error instanceof LogSizeExceededError ? '[Log output too large]' : String(value); + } + }) + .join(', '); } // this exists because in production mode, the ivy internal props are defined as diff --git a/projects/live-preview/components/si-live-preview/si-live-preview.component.ts b/projects/live-preview/components/si-live-preview/si-live-preview.component.ts index ec5385357e..f620783f56 100644 --- a/projects/live-preview/components/si-live-preview/si-live-preview.component.ts +++ b/projects/live-preview/components/si-live-preview/si-live-preview.component.ts @@ -34,6 +34,8 @@ import { SiLivePreviewLocaleApi } from '../../interfaces/si-live-preview.api'; import { SiLivePreviewIframeComponent } from '../si-live-preview-iframe/si-live-preview-iframe.component'; import { SiStackblitzButtonDirective } from '../stackblitz/si-stackblitz-button.component'; +const MAX_LOG_MESSAGES = 100; + @Component({ selector: 'si-live-preview', imports: [KeyValuePipe, FormsModule, SiLivePreviewIframeComponent, SiStackblitzButtonDirective], @@ -120,6 +122,7 @@ export class SiLivePreviewComponent implements OnInit, AfterViewInit, OnChanges private jsLoaded = false; private delayClearTimer: any; private webcomponentsList: string[] = []; + private maxLogMessages = this.config.maxLogMessages ?? MAX_LOG_MESSAGES; constructor() { this.compileSubject @@ -514,7 +517,8 @@ export class SiLivePreviewComponent implements OnInit, AfterViewInit, OnChanges clearTimeout(this.delayClearTimer); this.delayClearTimer = undefined; } - this.logMessages.set([...this.logMessages(), msg]); + + this.logMessages.update(messages => [...messages.slice(-(this.maxLogMessages - 1)), msg]); this.newMsgs.set(true); setTimeout(() => { this.consoleElem().nativeElement.scrollTop = this.consoleElem().nativeElement.scrollHeight; diff --git a/projects/live-preview/interfaces/live-preview-config.ts b/projects/live-preview/interfaces/live-preview-config.ts index a42e8d6707..51f6e6eba2 100644 --- a/projects/live-preview/interfaces/live-preview-config.ts +++ b/projects/live-preview/interfaces/live-preview-config.ts @@ -30,6 +30,8 @@ export interface SiLivePreviewConfig { landscapeToggle?: boolean; webcomponents?: boolean; rootFontSizes?: number[]; + /** Maximum number of log messages to retain. */ + maxLogMessages?: number; } export interface SiLivePreviewInternals {