1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-10-13 22:41:57 +00:00

The Report Your Errors Update

* Add automatic error reporting with Sentry.io
* Filter a bunch of bad errors from showing up on Sentry
* Add module.hasModule method.
* Fix deep_copy
* Fix disallow mouse interaction with extensions
* Add some new icons to the icon font for mod cards
* Allow Ctrl-Shift-Clicking emotes.
* Rarity sorting for experiments and unset display for unused experiments.
This commit is contained in:
SirStendec 2018-04-11 17:05:31 -04:00
parent e3a7e3b64d
commit d7a07a5612
32 changed files with 575 additions and 83 deletions

View file

@ -8,6 +8,28 @@
import Module from 'utilities/module';
import {has, get} from 'utilities/object';
const BAD_ERRORS = [
'timeout',
'unable to load',
'error internal',
'Internal Server Error'
];
function skip_error(err) {
for(const m of BAD_ERRORS)
if ( err.message.includes(m) )
return true;
}
export class GQLError extends Error {
constructor(err) {
super(`${err.message}; Location: ${err.locations}`);
}
}
export default class Apollo extends Module {
constructor(...args) {
super(...args);
@ -17,6 +39,7 @@ export default class Apollo extends Module {
this.inject('..web_munch');
this.inject('..fine');
//this.inject('core');
}
onEnable() {
@ -62,6 +85,10 @@ export default class Apollo extends Module {
if ( ! this.enabled )
return forward(operation);
let vars = operation.variables;
if ( ! Object.keys(vars).length )
vars = undefined;
try {
// ONLY do this if we've hooked query init, thus letting us ignore certain issues
// that would cause Twitch to show lovely "Error loading data" messages everywhere.
@ -69,6 +96,14 @@ export default class Apollo extends Module {
this.apolloPreFlight(operation);
} catch(err) {
this.log.capture(err, {
tags: {
operation: operation.operationName
},
extra: {
variables: vars
}
});
this.log.error('Error running Pre-Flight', err, operation);
return forward(operation);
}
@ -80,9 +115,45 @@ export default class Apollo extends Module {
try {
out.subscribe({
next: result => {
if ( result.errors ) {
const name = operation.operationName;
if ( name.includes('FFZ') || has(this.modifiers, name) || has(this.post_modifiers, name) ) {
for(const err of result.errors) {
if ( skip_error(err) )
continue;
this.log.capture(new GQLError(err), {
tags: {
operation: operation.operationName
},
extra: {
variables: vars
}
});
}
}
}
this.log.crumb({
level: 'info',
category: 'gql',
message: `${operation.operationName} [${result.extensions && result.extensions.durationMilliseconds || '??'}ms]`,
data: {
variables: vars,
}
});
try {
this.apolloPostFlight(result);
} catch(err) {
this.log.capture(err, {
tags: {
operation: operation.operationName
},
extra: {
variables: vars
}
});
this.log.error('Error running Post-Flight', err, result);
}
@ -97,6 +168,14 @@ export default class Apollo extends Module {
});
} catch(err) {
this.log.capture(err, {
tags: {
operation: operation.operationName
},
extra: {
variables: vars
}
});
this.log.error('Link Error', err);
observer.error(err);
}

View file

@ -61,14 +61,14 @@ export default class FineRouter extends Module {
this.current = route;
this.current_name = route.name;
this.match = match;
this.emitSafe(':route', route, match);
this.emitSafe(`:route:${route.name}`, ...match);
this.emit(':route', route, match);
this.emit(`:route:${route.name}`, ...match);
return;
}
}
this.current = this.current_name = this.match = null;
this.emitSafe(':route', null, null);
this.emit(':route', null, null);
}
route(name, path) {

View file

@ -573,7 +573,13 @@ export class FineWrapper extends EventEmitter {
try {
inst.forceUpdate();
} catch(err) {
this.fine.log.error(`An error occured when calling forceUpdate on an instance of ${this.name}`, err);
this.fine.log.capture(err, {
tags: {
fine_wrapper: this.name
}
});
this.finelog.error(`An error occured when calling forceUpdate on an instance of ${this.name}`, err);
}
}

View file

@ -6,6 +6,8 @@ export const SERVER = DEBUG ? '//localhost:8000' : 'https://cdn.frankerfacez.com
export const CLIENT_ID = 'a3bc9znoz6vi8ozsoca0inlcr4fcvkl';
export const API_SERVER = '//api.frankerfacez.com';
export const SENTRY_ID = 'https://18f42c65339d4164b3fdebfc8c8bc99b@sentry.io/1186960';
export const TWITCH_EMOTE_BASE = '//static-cdn.jtvnw.net/emoticons/v1/';
export const KNOWN_CODES = {

View file

@ -178,7 +178,7 @@ export class ClickOutside {
}
handleClick(e) {
if ( ! this.el.contains(e.target) )
if ( this.el && ! this.el.contains(e.target) )
this.cb(e);
}
}

View file

@ -119,7 +119,7 @@ export class EventEmitter {
return list ? Array.from(list) : [];
}
emit(event, ...args) {
emitUnsafe(event, ...args) {
const list = this.__listeners[event];
if ( ! list )
return;
@ -160,28 +160,25 @@ export class EventEmitter {
}
}
emitSafe(event, ...args) {
try {
return [this.emit(event, ...args), undefined];
} catch(err) {
return [null, err];
}
}
emitAsync(event, ...args) {
emit(event, ...args) {
const list = this.__listeners[event];
if ( ! list )
return Promise.resolve([]);
return;
// Track removals separately to make iteration over the event list
// much, much simpler.
const removed = new Set,
promises = [];
const removed = new Set;
for(const item of list) {
const [fn, ctx, ttl] = item;
const ret = fn.apply(ctx, args);
let ret;
try {
ret = fn.apply(ctx, args);
} catch(err) {
if ( this.log )
this.log.capture(err, {tags: {event}, extra:{args}});
}
if ( ret === Detach )
removed.add(item);
else if ( ttl !== false ) {
@ -190,9 +187,6 @@ export class EventEmitter {
else
item[2] = ttl - 1;
}
if ( ret !== Detach )
promises.push(ret);
}
if ( removed.size ) {
@ -211,8 +205,72 @@ export class EventEmitter {
}
}
}
}
return Promise.all(promises);
async emitAsync(event, ...args) {
const list = this.__listeners[event];
if ( ! list )
return [];
// Track removals separately to make iteration over the event list
// much, much simpler.
const removed = new Set,
promises = [];
for(const item of list) {
const [fn, ctx] = item;
let ret;
try {
ret = fn.apply(ctx, args);
} catch(err) {
if ( this.log )
this.log.capture(err, {tags: {event}, extra: {args}});
}
if ( !(ret instanceof Promise) )
ret = Promise.resolve(ret);
promises.push(ret.then(r => {
const new_ttl = item[2];
if ( r === Detach )
removed.add(item);
else if ( new_ttl !== false ) {
if ( new_ttl <= 1 )
removed.add(item);
else
item[2] = new_ttl - 1;
}
if ( ret !== Detach )
return ret;
}).catch(err => {
if ( this.log )
this.log.capture(err, {event, args});
return null;
}));
}
const out = await Promise.all(promises);
if ( removed.size ) {
// Re-grab the list to make sure it wasn't removed mid-iteration.
const new_list = this.__listeners[event];
if ( new_list ) {
for(const item of removed) {
const idx = new_list.indexOf(item);
if ( idx !== -1 )
new_list.splice(idx, 1);
}
if ( ! list.length ) {
this.__listeners[event] = null;
this.__dead_events++;
}
}
}
return out;
}
}

View file

@ -1,12 +1,21 @@
'use strict';
const RAVEN_LEVELS = {
1: 'debug',
2: 'info',
4: 'warn',
8: 'error'
};
export default class Logger {
constructor(parent, name, level) {
constructor(parent, name, level, raven) {
this.parent = parent;
this.name = name;
this.enabled = true;
this.level = level || (parent && parent.level) || Logger.DEFAULT_LEVEL;
this.raven = raven || (parent && parent.raven);
this.children = {};
}
@ -34,6 +43,24 @@ export default class Logger {
return this.invoke(Logger.ERROR, args);
}
crumb(...args) {
if ( this.raven )
return this.raven.captureBreadcrumb(...args);
}
capture(exc, opts, ...args) {
if ( this.raven ) {
opts = opts || {};
if ( ! opts.logger )
opts.logger = this.name;
this.raven.captureException(exc, opts);
}
if ( args.length )
return this.error(...args);
}
/* eslint no-console: "off" */
invoke(level, args) {
if ( ! this.enabled || level < this.level )
@ -41,6 +68,12 @@ export default class Logger {
const message = Array.prototype.slice.call(args);
this.crumb({
message: message.join(' '),
category: this.name,
level: RAVEN_LEVELS[level] || level
});
if ( this.name )
message.unshift(`%cFFZ [%c${this.name}%c]:%c`, 'color:#755000; font-weight:bold', '', 'color:#755000; font-weight:bold', '');
else

View file

@ -380,6 +380,12 @@ export class Module extends EventEmitter {
}
hasModule(name) {
const module = this.__modules[this.abs_path(name)];
return module instanceof Module;
}
__get_requires() {
if ( has(this, 'requires') )
return this.requires;
@ -430,6 +436,8 @@ export class Module extends EventEmitter {
else
this.__modules[this.abs_path(full_name)] = [[], [], [[this.__path, name]]]
requires.push(this.abs_path(full_name));
return this[name] = null;
}

View file

@ -198,14 +198,14 @@ export function deep_copy(object, seen) {
seen.add(object);
if ( Array.isArray(object) )
return object.map(x => deep_copy(x, seen));
return object.map(x => deep_copy(x, new Set(seen)));
const out = {};
for(const key in object)
if ( HOP.call(object, key) ) {
const val = object[key];
if ( typeof val === 'object' )
out[key] = deep_copy(val, seen);
out[key] = deep_copy(val, new Set(seen));
else
out[key] = val;
}

View file

@ -18,10 +18,14 @@ export class Vue extends Module {
async onLoad() {
const Vue = window.ffzVue = this.Vue = (await import(/* webpackChunkName: "vue" */ 'vue')).default,
RavenVue = await import(/* webpackChunkName: "vue" */ 'raven-js/plugins/vue'),
components = this._components;
this.component((await import(/* webpackChunkName: "vue" */ 'src/std-components/index.js')).default);
if ( this.root.raven )
this.root.raven.addPlugin(RavenVue, Vue);
for(const key in components)
if ( has(components, key) )
Vue.component(key, components[key]);