Skip to content

Commit

Permalink
feat: make Deno capture unhandled exceptions and rejections and repor…
Browse files Browse the repository at this point in the history
…t them to the server (#33997)
  • Loading branch information
d-gubert authored Nov 20, 2024
1 parent 79bbbd6 commit ce7024a
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .changeset/three-dragons-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@rocket.chat/apps-engine': minor
'@rocket.chat/livechat': minor
'@rocket.chat/i18n': minor
'@rocket.chat/meteor': minor
---

Prevent apps' subprocesses from crashing on unhandled rejections or uncaught exceptions
33 changes: 33 additions & 0 deletions packages/apps-engine/deno-runtime/error-handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as Messenger from './lib/messenger.ts';

export function unhandledRejectionListener(event: PromiseRejectionEvent) {
event.preventDefault();

const { type, reason } = event;

Messenger.sendNotification({
method: 'unhandledRejection',
params: [
{
type,
reason: reason instanceof Error ? reason.message : reason,
timestamp: new Date(),
},
],
});
}

export function unhandledExceptionListener(event: ErrorEvent) {
event.preventDefault();

const { type, message, filename, lineno, colno } = event;
Messenger.sendNotification({
method: 'uncaughtException',
params: [{ type, message, filename, lineno, colno }],
});
}

export default function registerErrorListeners() {
addEventListener('unhandledrejection', unhandledRejectionListener);
addEventListener('error', unhandledExceptionListener);
}
3 changes: 3 additions & 0 deletions packages/apps-engine/deno-runtime/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import videoConferenceHandler from './handlers/videoconference-handler.ts';
import apiHandler from './handlers/api-handler.ts';
import handleApp from './handlers/app/handler.ts';
import handleScheduler from './handlers/scheduler-handler.ts';
import registerErrorListeners from './error-handlers.ts';

type Handlers = {
app: typeof handleApp;
Expand Down Expand Up @@ -126,4 +127,6 @@ async function main() {
}
}

registerErrorListeners();

main();
2 changes: 2 additions & 0 deletions packages/apps-engine/src/definition/metadata/AppMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,6 @@ export enum AppMethod {
EXECUTE_POST_USER_STATUS_CHANGED = 'executePostUserStatusChanged',
// Runtime specific methods
RUNTIME_RESTART = 'runtime:restart',
RUNTIME_UNCAUGHT_EXCEPTION = 'runtime:uncaughtException',
RUNTIME_UNHANDLED_REJECTION = 'runtime:unhandledRejection',
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { IParseAppPackageResult } from '../../compiler';
import { AppConsole, type ILoggerStorageEntry } from '../../logging';
import type { AppAccessorManager, AppApiManager } from '../../managers';
import type { AppLogStorage, IAppStorageItem } from '../../storage';
import { AppMethod } from '../../../definition/metadata';

const baseDebug = debugFactory('appsEngine:runtime:deno');

Expand Down Expand Up @@ -367,7 +368,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
this.deno.stderr.on('data', this.parseError.bind(this));
this.deno.on('error', (err) => {
this.state = 'invalid';
console.error('Failed to startup Deno subprocess', err);
console.error(`Failed to startup Deno subprocess for app ${this.getAppId()}`, err);
});
this.once('ready', this.onReady.bind(this));
this.parseStdout(this.deno.stdout);
Expand Down Expand Up @@ -544,12 +545,28 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
case 'log':
console.log('SUBPROCESS LOG', message);
break;
case 'unhandledRejection':
case 'uncaughtException':
await this.logUnhandledError(`runtime:${method}`, message);
break;
default:
console.warn('Unrecognized method from sub process');
break;
}
}

private async logUnhandledError(
method: `${AppMethod.RUNTIME_UNCAUGHT_EXCEPTION | AppMethod.RUNTIME_UNHANDLED_REJECTION}`,
message: jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification,
) {
this.debug('Unhandled error of type "%s" caught in subprocess', method);

const logger = new AppConsole(method);
logger.error(message.payload);

await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger));
}

private async handleResultMessage(message: jsonrpc.IParsedObjectError | jsonrpc.IParsedObjectSuccess): Promise<void> {
const { id } = message.payload;

Expand Down

0 comments on commit ce7024a

Please sign in to comment.