Skip to content

Commit

Permalink
v1.9.1.0: Detect packages involved in unhandled errors
Browse files Browse the repository at this point in the history
  • Loading branch information
ruipin committed Aug 25, 2021
1 parent fe42707 commit 983c8fb
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 23 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.9.1.0 (2021-08-25)

- When an unhandled exception is seen by libWrapper, it will detect involved packages (if any) and append this list to the exception message.

# 1.9.0.0 (2021-08-23)

- Support wrapping global methods when they are available in `globalThis` and the associated descriptor has `configurable: true`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,6 @@ The largest community-provided support channels are:
⚠ *Do not open a support ticket using the link below unless you are seeing an **internal libWrapper error** or are a **package developer**. We also do not provide support for packages that promote or otherwise endorse piracy. Your issue will be closed as invalid if you do not fulfill these requirements.*
If you encounter an internal libWrapper error, or are a module developer looking for support (i.e. bug reports, feature requests, questions, etc), you may get in touch by opening a new issue on the [libWrapper issue tracker](https://github.com/ruipin/fvtt-lib-wrapper/issues). It is usually a good idea to search the existing issues first in case yours has already been answered before.
If you encounter an internal libWrapper error, or are a package developer looking for support (i.e. bug reports, feature requests, questions, etc), you may get in touch by opening a new issue on the [libWrapper issue tracker](https://github.com/ruipin/fvtt-lib-wrapper/issues). It is usually a good idea to search the existing issues first in case yours has already been answered before.
If your support request relates to an error, please describe with as much detail as possible the error you are seeing, and what you have already done to troubleshoot it. Providing a step-by-step description of how to reproduce it or a snippet of code that triggers the issue is especially welcome, and will ensure you get an answer as fast as possible.
2 changes: 1 addition & 1 deletion module.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "lib-wrapper",
"title": "libWrapper",
"description": "Library for wrapping core Foundry VTT methods, meant to improve compatibility between packages that wrap the same methods.",
"version": "1.9.0.0",
"version": "1.9.1.0",
"author": "Rui Pinheiro",
"esmodules": ["src/index.js"],
"styles": ["dist/lib-wrapper.css"],
Expand Down
8 changes: 7 additions & 1 deletion src/errors/base_errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

'use strict';

import {PackageInfo} from '../shared/package_info.js';
import { PACKAGE_ID } from '../consts.js';
import { PackageInfo } from '../shared/package_info.js';
import { inject_packages_into_error } from './error-utils.js';


// Custom libWrapper Error
Expand All @@ -20,6 +22,10 @@ export class LibWrapperError extends Error {
this.ui_msg = ui_msg;
this.console_msg = console_msg;
this.notification_fn = notification_fn ?? 'error';

// Detect packages, inject them into error message
// Note: We hide 'lib-wrapper' from the list of detected packages, except when this was a libWrapper-internal error
inject_packages_into_error(this, this instanceof LibWrapperInternalError ? null : PACKAGE_ID);
}

/**
Expand Down
130 changes: 130 additions & 0 deletions src/errors/error-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro

'use strict';

import { PACKAGE_ID } from '../consts.js';
import { PackageInfo, PACKAGE_TYPES } from '../shared/package_info.js';


/*
* Utility methods for exceptions
*/
export function is_error_object(obj) {
// We ignore anything that is not an object
if(obj === null || obj === undefined || typeof obj !== 'object')
return false;

// We figure out if this cause has a message and a stack frame - i.e. duck typing of an error object
if(!('message' in obj) || !('stack' in obj))
return false;

// This is (probably) an error
return true;
}


function get_involved_packages(stack, ignore_ids=undefined) {
return PackageInfo.collect_all(stack, /* include_fn= */ (id, type, match) => {
// We don't include libWrapper's listeners.js file since that file shows up in almost every single stack trace
return id !== PACKAGE_ID || type !== PACKAGE_TYPES.MODULE || !match.includes('/listeners.js')
}, ignore_ids);
}


function get_involved_packages_message(stack, ignore_ids=undefined) {
const packages = get_involved_packages(stack, ignore_ids);
const length = packages.length;

// Zero packages
if(length <= 0)
return "[No packages detected]";

// 1 package
if(length == 1)
return `[Detected 1 package: ${packages[0].logId}]`;

// 2+ packages
return`[Detected ${length} packages: ${packages.map((p)=>p.logId).join(', ')}]`;
}


function has_property_string_writable(obj, prop) {
if(!(prop in obj))
return false

// Get the property's descriptor if available
const desc = Object.getOwnPropertyDescriptor(obj, prop);
if(desc) {
// Check if the descriptor is not a getter/setter
if(!('value' in desc))
return false;

// Check if the value is a string
if(typeof desc.value !== 'string')
return false;

// Check if it is writable
if(!desc.writable)
return false;
}
// We assume that if the property descriptor doesn't exist, then it is writable by default
// But we still need to validate that it is a string
else {
const value = obj[prop];

if(typeof value !== 'string')
return false;
}

// Done
return true;
}


function can_inject_message(error) {
// Can't modify a frozen object
if(Object.isFrozen(error))
return false;

// We need both 'message' and 'stack' to be writable strings
if(!has_property_string_writable(error, 'message') || !has_property_string_writable(error, 'stack'))
return false;

// Done
return true;
}


export function inject_packages_into_error(error, ignore_ids=undefined) {
// Sanity check
if(!is_error_object(error))
return;

// Skip package detection is already marked
if(error.skip_package_detection)
return;

// Test whether error object allows injection
if(!can_inject_message(error))
return;

// Generate involved packages string
const packages_str = get_involved_packages_message(error.stack, ignore_ids);

// Not necessary to inject a second time, if already present
if(error.message.endsWith(packages_str)) {
error.skip_package_detection = true;
return;
}

// Append to error message
const orig_msg = error.message;
error.message += `\n${packages_str}`;

// If the stack contains the error message, replace that as well
error.stack = error.stack.replace(orig_msg, error.message);

// Done - signal this error doesn't need package detection any more
error.skip_package_detection = true;
}
80 changes: 61 additions & 19 deletions src/errors/listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,84 @@

'use strict';

import {IS_UNITTEST, DEBUG} from '../consts.js';
import {global_eval} from '../utils/misc.js';
import {LibWrapperError} from './base_errors.js';
import {LibWrapperNotifications} from '../ui/notifications.js';
import { IS_UNITTEST, DEBUG } from '../consts.js';
import { global_eval } from '../utils/misc.js';
import { LibWrapperError } from './base_errors.js';
import { is_error_object, inject_packages_into_error } from './error-utils.js';
import { LibWrapperNotifications } from '../ui/notifications.js';


// Error listeners for unhandled exceptions
export const onUnhandledError = function(event) {
// This is a LibWrapperError exception, and we need to handle it
/*
* Make sure browser is allowed to collect full stack traces, for easier debugging of issues
*/
Error.stackTraceLimit = Infinity;


/*
* Utility Methods
*/
function on_libwrapper_error(error) {
// Notify user of the issue
if(error.ui_msg && error.notification_fn)
LibWrapperNotifications.ui(`${error.ui_msg} (See JS console)`, error.notification_fn);

// Trigger 'onUnhandled'
if(error.onUnhandled)
error.onUnhandled.apply(error);
}

function on_any_error(error) {
// Detect packages and inject a list into the error object
inject_packages_into_error(error);
}


/*
* Error Listeners
*/
export const onUnhandledError = function(error) {
try {
// We first check whether the cause of the event is an instance of LibWrapperError. Otherwise, we do nothing.
const exc = event.reason ?? event.error ?? event;
if(!exc || !(exc instanceof LibWrapperError))
// Sanity check
if(!is_error_object(error))
return;

// Notify user of the issue
if(exc.ui_msg && exc.notification_fn)
LibWrapperNotifications.ui(`${exc.ui_msg} (See JS console)`, exc.notification_fn);
// If we have an instance of LibWrapperError, we trigger the libWrapper-specific behaviour
if(error instanceof LibWrapperError)
on_libwrapper_error(error);

// Trigger 'onUnhandled'
if(exc.onUnhandled)
exc.onUnhandled.apply(exc);
// Trigger the error handling code for all errors
on_any_error(error);
}
catch (e) {
console.warn('libWrapper: Exception thrown while processing an unhandled exception.', e);
console.warn('libWrapper: Exception thrown while processing an unhandled error.', e);
}
}

const onUnhandledErrorEvent = function(event) {
try {
// The cause of the event is what we're interested in
const cause = event.reason ?? event.error ?? event;

// We've got our error object, call onUnhandledError
return onUnhandledError(cause);
}
catch (e) {
console.warn('libWrapper: Exception thrown while processing an unhandled error event.', e);
}
}


/*
* Set up error listeners
*/
export const init_error_listeners = function() {
// Do nothing inside unit tests
if(IS_UNITTEST)
return;

// Javascript native unhandled exception listeners
globalThis.addEventListener('error', onUnhandledError);
globalThis.addEventListener('unhandledrejection', onUnhandledError);
globalThis.addEventListener('error', onUnhandledErrorEvent);
globalThis.addEventListener('unhandledrejection', onUnhandledErrorEvent);

// Wrap Hooks._call to intercept unhandled exceptions during hooks
// We don't use libWrapper itself here as we can guarantee we come first (well, before any libWrapper wrapper) and we want to avoid polluting the callstack of every single hook.
Expand Down
2 changes: 1 addition & 1 deletion src/shared
Submodule shared updated 1 files
+25 −10 package_info.js

0 comments on commit 983c8fb

Please sign in to comment.