From 1c93b7635ae35083c81bb50e1b7ade7c417e547d Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Tue, 24 Sep 2024 17:07:55 +0300 Subject: [PATCH] add support for console replay of console logs happen in async operations get console replay messages after promise resolves or rejects use isPromise to check if the result is a promise make consoleReplay function accept the list of logs to be added to the script make consoleHistory argument of consoleReplay optional add comments add comments update comment remove FlowFixMe comment --- node_package/src/buildConsoleReplay.ts | 12 ++++---- .../src/serverRenderReactComponent.ts | 30 +++++++++++-------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/node_package/src/buildConsoleReplay.ts b/node_package/src/buildConsoleReplay.ts index c1fdafcea..f39cec428 100644 --- a/node_package/src/buildConsoleReplay.ts +++ b/node_package/src/buildConsoleReplay.ts @@ -9,13 +9,15 @@ declare global { } } -export function consoleReplay(): string { +export function consoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined): string { // console.history is a global polyfill used in server rendering. - if (!(console.history instanceof Array)) { + const consoleHistory = customConsoleHistory ?? console.history; + + if (!(Array.isArray(consoleHistory))) { return ''; } - const lines = console.history.map(msg => { + const lines = consoleHistory.map(msg => { const stringifiedList = msg.arguments.map(arg => { let val: string; try { @@ -42,6 +44,6 @@ export function consoleReplay(): string { return lines.join('\n'); } -export default function buildConsoleReplay(): string { - return RenderUtils.wrapInScriptTags('consoleReplayLog', consoleReplay()); +export default function buildConsoleReplay(customConsoleHistory: typeof console['history'] | undefined = undefined): string { + return RenderUtils.wrapInScriptTags('consoleReplayLog', consoleReplay(customConsoleHistory)); } diff --git a/node_package/src/serverRenderReactComponent.ts b/node_package/src/serverRenderReactComponent.ts index 2e84aea45..6f5c5ee26 100644 --- a/node_package/src/serverRenderReactComponent.ts +++ b/node_package/src/serverRenderReactComponent.ts @@ -105,15 +105,21 @@ function createResultObject(html: string | null, consoleReplayScript: string, ha async function createPromiseResult( renderState: RenderState & { result: Promise }, - consoleReplayScript: string, componentName: string, throwJsErrors: boolean ): Promise { + // Capture console history before awaiting the promise + // Node renderer will reset the global console.history after executing the synchronous part of the request. + // It resets it only if replayServerAsyncOperationLogs renderer config is set to false. + // In both cases, we need to keep a reference to console.history to avoid losing console logs in case of reset. + const consoleHistory = console.history; try { const html = await renderState.result; + const consoleReplayScript = buildConsoleReplay(consoleHistory); return createResultObject(html, consoleReplayScript, renderState.hasErrors, renderState.error); } catch (e: unknown) { const errorRenderState = handleRenderingError(e, { componentName, throwJsErrors }); + const consoleReplayScript = buildConsoleReplay(consoleHistory); return createResultObject(errorRenderState.result, consoleReplayScript, errorRenderState.hasErrors, errorRenderState.error); } } @@ -123,20 +129,12 @@ function createFinalResult( componentName: string, throwJsErrors: boolean ): null | string | Promise { - // Console history is stored globally in `console.history`. - // If node renderer is handling a render request that returns a promise, - // It can handle another request while awaiting the promise. - // To prevent cross-request console logs leakage between these requests, - // we build the consoleReplayScript before awaiting any promises. - // The console history is reset after the synchronous part of each request. - // This causes console logs happening during async operations to not be captured. - const consoleReplayScript = buildConsoleReplay(); - const { result } = renderState; if (isPromise(result)) { - return createPromiseResult({ ...renderState, result }, consoleReplayScript, componentName, throwJsErrors); + return createPromiseResult({ ...renderState, result }, componentName, throwJsErrors); } + const consoleReplayScript = buildConsoleReplay(); return JSON.stringify(createResultObject(result, consoleReplayScript, renderState.hasErrors, renderState.error)); } @@ -183,13 +181,19 @@ function serverRenderReactComponentInternal(options: RenderParams): null | strin } const serverRenderReactComponent: typeof serverRenderReactComponentInternal = (options) => { + let result: string | Promise | null = null; try { - return serverRenderReactComponentInternal(options); + result = serverRenderReactComponentInternal(options); } finally { // Reset console history after each render. // See `RubyEmbeddedJavaScript.console_polyfill` for initialization. - console.history = []; + // We don't need to clear the console history if the result is a promise + // Promises only supported in node renderer and node renderer takes care of cleanining console history + if (typeof result === 'string') { + console.history = []; + } } + return result; }; export default serverRenderReactComponent;