Expose arguments for `renderHydrogenToString` (pure function) so we can reproduce when error occurs while we render Hydrogen (#35)

`renderHydrogenToString` is a pure function (probably) which means it will give the same output given the same input. This means, that if we give it a certain input and an error occurs, we should be able to reproduce it again if we have the arguments. This PR exposes those arguments in the logged error so we can investigate what's going wrong.

Added so we can investigate https://github.com/matrix-org/matrix-public-archive/issues/34 better and reproduce locally.
This commit is contained in:
Eric Eastwood 2022-07-05 10:30:12 -05:00 committed by GitHub
parent d508521171
commit 17f2c399dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 142 additions and 71 deletions

View File

@ -0,0 +1,61 @@
'use strict';
// via https://stackoverflow.com/a/42755876/796832
// Standard error extender from @deployable/errors
class ExtendedError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
this.message = message;
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = new Error(message).stack;
}
}
}
// A way to create a new error with a custom message but keep the stack trace of
// the original error. Useful to give more context and why the action was tried
// in the first place.
//
// For example, if you get a generic EACCES disk error of a certain file, you
// want to know why and what context the disk was trying to be read. A
// stack-trace is not human digestable and only gives the where in the code.
// What I actually need to know is that I was trying to read the `ratelimit` key
// from the config when this error occured.
//
// `new RethrownError('Failed to get the ratelimit key from the config', originalError)` (failed to read the disk)
class RethrownError extends ExtendedError {
constructor(message, error) {
super(message);
if (!error) throw new Error('RethrownError requires a message and error');
this.original = error;
this.newStack = this.stack;
// The number of lines that make up the message itself. We count this by the
// number of `\n` and `+ 1` for the first line because it doesn't start with
// new line.
const messageLines = (this.message.match(/\n/g) || []).length + 1;
console.log('messageLines', messageLines);
const indentedOriginalError = error.stack
.split('\n')
.map((line) => ` ${line}`)
.join('\n');
this.stack =
this.stack
.split('\n')
// We use `+ 1` here so that we include the first line of the stack to
// people know where the error was thrown from.
.slice(0, messageLines + 1)
.join('\n') +
'\n' +
' --- Original Error ---\n' +
indentedOriginalError;
}
}
module.exports = RethrownError;

View File

@ -7,82 +7,92 @@ const { readFile } = require('fs').promises;
const crypto = require('crypto');
const { parseHTML } = require('linkedom');
const RethrownError = require('./lib/rethrown-error');
const config = require('./lib/config');
async function renderToString({ fromTimestamp, roomData, events, stateEventMap }) {
assert(fromTimestamp);
assert(roomData);
assert(events);
assert(stateEventMap);
async function renderHydrogenToString({ fromTimestamp, roomData, events, stateEventMap }) {
try {
assert(fromTimestamp);
assert(roomData);
assert(events);
assert(stateEventMap);
const dom = parseHTML(`
<!doctype html>
<html>
<head></head>
<body>
<div id="app" class="hydrogen"></div>
</body>
</html>
`);
const dom = parseHTML(`
<!doctype html>
<html>
<head></head>
<body>
<div id="app" class="hydrogen"></div>
</body>
</html>
`);
if (!dom.requestAnimationFrame) {
dom.requestAnimationFrame = function (cb) {
setTimeout(cb, 0);
if (!dom.requestAnimationFrame) {
dom.requestAnimationFrame = function (cb) {
setTimeout(cb, 0);
};
}
// Define this for the SSR context
dom.window.matrixPublicArchiveContext = {
fromTimestamp,
roomData,
events,
stateEventMap,
config: {
basePort: config.get('basePort'),
basePath: config.get('basePath'),
matrixServerUrl: config.get('matrixServerUrl'),
},
};
// Serialize it for when we run this again client-side
dom.document.body.insertAdjacentHTML(
'beforeend',
`
<script type="text/javascript">
window.matrixPublicArchiveContext = ${JSON.stringify(dom.window.matrixPublicArchiveContext)}
</script>
`
);
const vmContext = vm.createContext(dom);
// Make the dom properties available in sub-`require(...)` calls
vmContext.global.window = dom.window;
vmContext.global.document = dom.document;
vmContext.global.Node = dom.Node;
vmContext.global.navigator = dom.navigator;
vmContext.global.DOMParser = dom.DOMParser;
// Make sure `webcrypto` exists since it was only introduced in Node.js v17
assert(crypto.webcrypto);
vmContext.global.crypto = crypto.webcrypto;
// So require(...) works in the vm
vmContext.global.require = require;
// So we can see logs from the underlying vm
vmContext.global.console = console;
const hydrogenRenderScriptCode = await readFile(
path.resolve(__dirname, '../shared/hydrogen-vm-render-script.js'),
'utf8'
);
const hydrogenRenderScript = new vm.Script(hydrogenRenderScriptCode, {
filename: 'hydrogen-vm-render-script.js',
});
const vmResult = hydrogenRenderScript.runInContext(vmContext);
// Wait for everything to render
// (waiting on the promise returned from `hydrogen-render-script.js`)
await vmResult;
const documentString = dom.document.body.toString();
return documentString;
} catch (err) {
throw new RethrownError(
`Failed to render Hydrogen to string. In order to reproduce, feed in these arguments into \`renderHydrogenToString(...)\`:\n renderToString arguments: ${JSON.stringify(
arguments[0]
)}`,
err
);
}
// Define this for the SSR context
dom.window.matrixPublicArchiveContext = {
fromTimestamp,
roomData,
events,
stateEventMap,
config: {
basePort: config.get('basePort'),
basePath: config.get('basePath'),
matrixServerUrl: config.get('matrixServerUrl'),
},
};
// Serialize it for when we run this again client-side
dom.document.body.insertAdjacentHTML(
'beforeend',
`
<script type="text/javascript">
window.matrixPublicArchiveContext = ${JSON.stringify(dom.window.matrixPublicArchiveContext)}
</script>
`
);
const vmContext = vm.createContext(dom);
// Make the dom properties available in sub-`require(...)` calls
vmContext.global.window = dom.window;
vmContext.global.document = dom.document;
vmContext.global.Node = dom.Node;
vmContext.global.navigator = dom.navigator;
vmContext.global.DOMParser = dom.DOMParser;
// Make sure `webcrypto` exists since it was only introduced in Node.js v17
assert(crypto.webcrypto);
vmContext.global.crypto = crypto.webcrypto;
// So require(...) works in the vm
vmContext.global.require = require;
// So we can see logs from the underlying vm
vmContext.global.console = console;
const hydrogenRenderScriptCode = await readFile(
path.resolve(__dirname, '../shared/hydrogen-vm-render-script.js'),
'utf8'
);
const hydrogenRenderScript = new vm.Script(hydrogenRenderScriptCode, {
filename: 'hydrogen-vm-render-script.js',
});
const vmResult = hydrogenRenderScript.runInContext(vmContext);
// Wait for everything to render
// (waiting on the promise returned from `hydrogen-render-script.js`)
await vmResult;
const documentString = dom.document.body.toString();
return documentString;
}
module.exports = renderToString;
module.exports = renderHydrogenToString;