Skip to content

Commit

Permalink
Build console replay script after awaiting the rendering promise (#1649)
Browse files Browse the repository at this point in the history
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
shakacode/react_on_rails_pro#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 2697ef7.

* add comment about clearing console history
  • Loading branch information
AbanoubGhadban authored Oct 20, 2024
1 parent 616fe9f commit eb7639b
Show file tree
Hide file tree
Showing 2 changed files with 19 additions and 17 deletions.
12 changes: 7 additions & 5 deletions node_package/src/buildConsoleReplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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));
}
24 changes: 12 additions & 12 deletions node_package/src/serverRenderReactComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,22 @@ function createResultObject(html: string | null, consoleReplayScript: string, re

async function createPromiseResult(
renderState: RenderState & { result: Promise<string> },
consoleReplayScript: string,
componentName: string,
throwJsErrors: boolean
): Promise<RenderResult> {
// 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);
}
}

Expand All @@ -123,20 +129,12 @@ function createFinalResult(
componentName: string,
throwJsErrors: boolean
): null | string | Promise<RenderResult> {
// 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));
}

Expand Down Expand Up @@ -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 = [];
}
};
Expand Down

0 comments on commit eb7639b

Please sign in to comment.