From eb7639bacc8313599961eae5186b6ac3b3f21095 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Sun, 20 Oct 2024 14:38:57 +0300 Subject: [PATCH] Build console replay script after awaiting the rendering promise (#1649) Console replay script generation now awaits the render request promise before generating, allowing it to capture console logs from asynchronous operations. The feature of replaying async console logs is implemented in node-renderer in this PR https://github.com/shakacode/react_on_rails_pro/pull/440 * 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 * call clear console history from the generated render code * Revert "call clear console history from the generated render code" This reverts commit 2697ef729d209c71230a94a0522360ad4cdea92d. * add comment about clearing console history --- node_package/src/buildConsoleReplay.ts | 12 ++++++---- .../src/serverRenderReactComponent.ts | 24 +++++++++---------- 2 files changed, 19 insertions(+), 17 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 fec68e231..bbc1b53ef 100644 --- a/node_package/src/serverRenderReactComponent.ts +++ b/node_package/src/serverRenderReactComponent.ts @@ -105,16 +105,22 @@ function createResultObject(html: string | null, consoleReplayScript: string, re 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); } catch (e: unknown) { const errorRenderState = handleRenderingError(e, { componentName, throwJsErrors }); - return createResultObject(errorRenderState.result, consoleReplayScript, errorRenderState); + const consoleReplayScript = buildConsoleReplay(consoleHistory); + return createResultObject(errorRenderState.result, consoleReplayScript, renderState); } } @@ -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)); } @@ -188,6 +186,8 @@ const serverRenderReactComponent: typeof serverRenderReactComponentInternal = (o } finally { // Reset console history after each render. // See `RubyEmbeddedJavaScript.console_polyfill` for initialization. + // This is necessary when ExecJS and old versions of node renderer are used. + // New versions of node renderer reset the console history automatically. console.history = []; } };