Skip to content

Commit

Permalink
add support for console replay of console logs happen in async operat…
Browse files Browse the repository at this point in the history
…ions

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
  • Loading branch information
AbanoubGhadban committed Oct 13, 2024
1 parent 45b61ba commit 1c93b76
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 18 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));
}
30 changes: 17 additions & 13 deletions node_package/src/serverRenderReactComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,21 @@ function createResultObject(html: string | null, consoleReplayScript: string, ha

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.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);
}
}
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.hasErrors, renderState.error));
}

Expand Down Expand Up @@ -183,13 +181,19 @@ function serverRenderReactComponentInternal(options: RenderParams): null | strin
}

const serverRenderReactComponent: typeof serverRenderReactComponentInternal = (options) => {
let result: string | Promise<RenderResult> | 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;

0 comments on commit 1c93b76

Please sign in to comment.