1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 05:15:54 +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

@ -1,3 +1,18 @@
<div class="list-header">4.0.0-beta2.6<span>@b85fa005ec1f3929cdd8</span> <time datetime="2018-04-11">(2018-04-11)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Changed: Filter a bunch of errors out from Sentry logging.</li>
<li>API Added: <code>module.hasModule(name)</code> method to test if a module is already installed.</li>
</ul>
<div class="list-header">4.0.0-beta2.5<span>@b3fb24504616675ad2b9</span> <time datetime="2018-04-11">(2018-04-11)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Automatic error reporting using Sentry.</li>
<li>Added: Rarity sorting for the Experiments debugging information.</li>
<li>Changed: Allow Ctrl-Shift-Clicking emotes to open their information pages.</li>
<li>Fixed: <code>deep_copy</code> erroneously thinking some objects were recursive.</li>
<li>Fixed: The option to disallow mouse mouse interaction with extensions was not functioning.</li>
</ul>
<div class="list-header">4.0.0-beta2.4<span>@b3fb24504616675ad2b9</span> <time datetime="2018-04-10">(2018-04-10)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Debugging > Experiments for viewing active experiment information.</li>

5
package-lock.json generated
View file

@ -7294,6 +7294,11 @@
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
"dev": true
},
"raven-js": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.24.1.tgz",
"integrity": "sha512-p+e+yoQbq4YgXDonYIRZNL/Kov6+t5L0UNEHZeYNzjOkNNCXcwQ1Vi3pulgGBaOjqXNipkFsbpmnH7YI+GPSjw=="
},
"raw-body": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",

View file

@ -54,6 +54,7 @@
"js-cookie": "^2.2.0",
"path-to-regexp": "^2.2.0",
"popper.js": "^1.14.2",
"raven-js": "^3.24.1",
"sortablejs": "^1.7.0",
"vue": "^2.5.16",
"vue-clickaway": "^2.1.0",

Binary file not shown.

View file

@ -76,6 +76,14 @@
<glyph glyph-name="arrows-cw" unicode="&#xe822;" d="M843 261q0-3 0-4-36-150-150-243t-267-93q-81 0-157 31t-136 88l-72-72q-11-11-25-11t-25 11-11 25v250q0 14 11 25t25 11h250q14 0 25-11t10-25-10-25l-77-77q40-36 90-57t105-20q74 0 139 37t104 99q6 10 30 66 4 13 16 13h107q8 0 13-6t5-12z m14 446v-250q0-14-10-25t-26-11h-250q-14 0-25 11t-10 25 10 25l77 77q-82 77-194 77-75 0-140-37t-104-99q-6-10-29-66-5-13-17-13h-111q-7 0-13 6t-5 12v4q36 150 151 243t268 93q81 0 158-31t137-88l72 72q11 11 25 11t26-11 10-25z" horiz-adv-x="857.1" />
<glyph glyph-name="ignore" unicode="&#xe823;" d="M813 141v-291l-233 194c-26-4-53-6-80-6-242 0-437 153-437 343 0 190 195 344 437 344s438-154 438-344c0-93-48-178-125-240z m-125 272h-375v-63h375v63z" horiz-adv-x="1000" />
<glyph glyph-name="block" unicode="&#xe824;" d="M732 352q0 90-48 164l-421-420q76-50 166-50 62 0 118 25t96 65 65 97 24 119z m-557-167l421 421q-75 50-167 50-83 0-153-40t-110-111-41-153q0-91 50-167z m682 167q0-88-34-168t-91-137-137-92-166-34-167 34-137 92-91 137-34 168 34 167 91 137 137 91 167 34 166-34 137-91 91-137 34-167z" horiz-adv-x="857.1" />
<glyph glyph-name="pin" unicode="&#xe825;" d="M573 37q0-23-15-38t-37-15q-21 0-37 16l-169 169-315-236 236 315-168 169q-24 23-12 56 14 32 48 32 157 0 270 57 90 45 151 171 9 24 36 32t50-13l208-209q21-23 14-50t-32-36q-127-63-172-152-56-110-56-268z" horiz-adv-x="834" />
<glyph glyph-name="pin-outline" unicode="&#xe826;" d="M856 554q30-30 30-73t-30-75q-16-16-36-24-106-51-144-125-51-102-51-246 0-45-29-74t-75-30q-47 0-75 30l-167 169-279-199 199 279-169 168q-20 18-27 49t5 64q27 65 96 65 146 0 246 50l11 7q69 37 118 141 7 20 21 34 30 30 74 29t74-31z m-273-250q53 107 196 175l-205 211q-69-146-176-200l-13-6q-120-57-281-57l416-416q0 168 63 293z" horiz-adv-x="886" />
<glyph glyph-name="twitter" unicode="&#xf099;" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />
<glyph glyph-name="gauge" unicode="&#xf0e4;" d="M214 207q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -18,6 +18,12 @@ const OVERRIDE_COOKIE = 'experiment_overrides',
};
// We want to import this so that the file is included in the output.
// We don't load using this because we might want a newer file from the
// server.
import EXPERIMENTS from 'file-loader?name=[name].[hash].[ext]!./experiments.json'; // eslint-disable-line no-unused-vars
// ============================================================================
// Experiment Manager
// ============================================================================
@ -31,6 +37,9 @@ export default class ExperimentManager extends Module {
this.settings.addUI('experiments', {
path: 'Debugging > Experiments',
component: 'experiments',
unique_id: () => this.unique_id,
ffz_data: () => deep_copy(this.experiments),
twitch_data: () => deep_copy(this.getTwitchExperiments()),
@ -66,7 +75,7 @@ export default class ExperimentManager extends Module {
let data;
try {
data = await fetch(`${SERVER}/static/experiments.json?_=${Date.now()}`).then(r =>
data = await fetch(`${SERVER}/script/experiments.json?_=${Date.now()}`).then(r =>
r.ok ? r.json() : null);
} catch(err) {
@ -179,12 +188,11 @@ export default class ExperimentManager extends Module {
_rebuildTwitchKey(key, is_set, new_val) {
const core = this.resolve('site').getCore(),
exps = core.experiments;
exps = core.experiments,
if ( ! has(exps.assignments, key) )
return;
const old_val = exps.assignments[key];
old_val = has(exps.assignments, key) ?
exps.assignments[key] :
undefined;
if ( old_val !== new_val ) {
const value = is_set ? new_val : old_val;

View file

@ -1,5 +1,7 @@
'use strict';
import RavenLogger from './raven';
import Logger from 'utilities/logging';
import Module from 'utilities/module';
@ -24,7 +26,14 @@ class FrankerFaceZ extends Module {
this.__state = 0;
this.__modules.core = this;
this.log = new Logger(this);
// ========================================================================
// Error Reporting and Logging
// ========================================================================
//if ( ! DEBUG )
this.inject('raven', RavenLogger);
this.log = new Logger(null, null, null, this.raven);
this.core_log = this.log.get('core');
this.log.info(`FrankerFaceZ v${VER} (build ${VER.build})`);
@ -86,18 +95,12 @@ class FrankerFaceZ extends Module {
await Promise.all(promises);
}
/* eslint class-methods-use-this: off */
api(...args) {
return this._api.create(...args);
}
}
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
major: 4, minor: 0, revision: 0, extra: '-beta2.4',
major: 4, minor: 0, revision: 0, extra: '-beta2.7',
build: __webpack_hash__,
toString: () =>
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`

View file

@ -195,6 +195,35 @@ export default class Emotes extends Module {
const provider = ds.provider;
if ( event.shiftKey && this.parent.context.get('chat.click-emotes') ) {
let url;
if ( provider === 'twitch' )
url = `https://twitchemotes.com/emotes/${ds.id}`;
else if ( provider === 'ffz' ) {
const emote_set = this.emote_sets[ds.set],
emote = emote_set && emote_set.emotes[ds.id];
if ( ! emote )
return;
if ( emote.click_url )
url = emote.click_url;
else if ( ! emote_set.source )
url = `https://www.frankerfacez.com/emoticons/${emote.id}`;
}
if ( url ) {
const win = window.open();
win.opener = null;
win.location = url;
}
return true;
}
if ( event[MOD_KEY] ) {
// Favoriting Emotes
let source, id;
@ -225,35 +254,6 @@ export default class Emotes extends Module {
return true;
}
if ( event.shiftKey && this.parent.context.get('chat.click-emotes') ) {
let url;
if ( provider === 'twitch' )
url = `https://twitchemotes.com/emotes/${ds.id}`;
else if ( provider === 'ffz' ) {
const emote_set = this.emote_sets[ds.set],
emote = emote_set && emote_set.emotes[ds.id];
if ( ! emote )
return;
if ( emote.click_url )
url = emote.click_url;
else if ( ! emote_set.source )
url = `https://www.frankerfacez.com/emoticons/${emote.id}`;
}
if ( url ) {
const win = window.open();
win.opener = null;
win.location = url;
}
return true;
}
}

View file

@ -303,11 +303,15 @@ export default class Chat extends Module {
else if ( this.users[login] && ! no_login )
user = this.users[login];
else if ( no_create )
return null;
if ( user && user.destroyed )
user = null;
else
user = new User(this, null, id, login);
if ( ! user ) {
if ( no_create )
return null;
else
user = new User(this, null, id, login);
}
if ( id && id !== user.id ) {
// If the ID isn't what we expected, something is very wrong here.
@ -358,11 +362,15 @@ export default class Chat extends Module {
else if ( this.rooms[login] && ! no_login )
room = this.rooms[login];
else if ( no_create )
return null;
if ( room && room.destroyed )
room = null;
else
room = new Room(this, id, login);
if ( ! room ) {
if ( no_create )
return null;
else
room = new Room(this, id, login);
}
if ( id && id !== room.id ) {
// If the ID isn't what we expected, something is very wrong here.

View file

@ -70,8 +70,8 @@ export default class Room {
this.manager.socket.unsubscribe(this, `room.${this.login}`);
}
if ( this.manager.room_ids[this.id] === this )
this.manager.room_ids[this.id] = null;
if ( this.manager.room_ids[this._id] === this )
this.manager.room_ids[this._id] = null;
}

View file

@ -28,6 +28,14 @@ export default class User {
this.manager.emotes.unrefSet(set_id);
this.emote_sets = null;
const parent = this.room || this.manager;
if ( this._login && parent.users[this._login] === this )
parent.users[this._login] = null;
if ( parent.user_ids[this._id] === this )
parent.user_ids[this._id] = null;
}
get id() {

View file

@ -1,9 +1,23 @@
<template lang="html">
<div class="ffz--experiments tw-pd-t-05">
<div class="tw-pd-b-1 tw-mg-b-2 tw-border-b">
<div class="tw-pd-b-1 tw-mg-b-1 tw-border-b">
{{ t('settings.experiments.about', 'This feature allows you to override experiment values. Please note that, for most experiments, you may have to refresh the page for your changes to take effect.') }}
</div>
<div class="tw-mg-b-2 tw-flex tw-align-items-center">
<div class="tw-flex-grow-1">
{{ t('settings.experiments.unique-id', 'Unique ID: %{id}', {id: unique_id}) }}
</div>
<select
ref="sort_select"
class="tw-mg-x-05 tw-select tw-display-line tw-width-auto"
@change="onSort"
>
<option :selected="sort_by === 0">{{ t('settings.experiments.sort-name', 'Sort By: Name') }}</option>
<option :selected="sort_by === 1">{{ t('settings.experiments.sort-rarity', 'Sort By: Rarity') }}</option>
</select>
</div>
<h3 class="tw-mg-b-1">
{{ t('settings.experiments.ffz', 'FrankerFaceZ Experiments') }}
</h3>
@ -34,7 +48,7 @@
:key="idx"
:selected="i.value === exp.value"
>
{{ t('settings.exepriments.entry', '%{value} (weight: %{weight})', i) }}
{{ t('settings.experiments.entry', '%{value} (weight: %{weight})', i) }}
</option>
</select>
@ -97,6 +111,12 @@
class="tw-mg-05 tw-select tw-display-line tw-width-auto"
@change="onTwitchChange($event)"
>
<option
v-if="exp.in_use === false"
:selected="exp.default"
>
{{ t('settings.experiments.unset', 'unset') }}
</option>
<option
v-for="(i, idx) in exp.groups"
:key="idx"
@ -136,6 +156,8 @@ export default {
data() {
return {
sort_by: 1,
unique_id: this.item.unique_id(),
ffz_data: this.item.ffz_data(),
twitch_data: this.item.twitch_data()
}
@ -147,6 +169,10 @@ export default {
const exp = this.ffz_data[key];
this.$set(exp, 'value', this.item.getAssignment(key));
this.$set(exp, 'default', ! this.item.hasOverride(key));
exp.total = exp.groups.reduce((a,b) => a + b.weight, 0);
this.calculateRarity(exp);
}
for(const key in this.twitch_data)
@ -157,6 +183,8 @@ export default {
exp.in_use = this.item.usingTwitchExperiment(key);
exp.remainder = `v: ${exp.v}, t: ${exp.t}`;
exp.total = exp.groups.reduce((a,b) => a + b.weight, 0);
this.calculateRarity(exp);
}
this.item.on(':changed', this.valueChanged, this);
@ -169,6 +197,17 @@ export default {
},
methods: {
calculateRarity(exp) {
let rarity;
for(const group of exp.groups)
if ( group.value === exp.value ) {
rarity = group.weight / exp.total;
break;
}
this.$set(exp, 'rarity', rarity);
},
sorted(data) {
const out = Object.entries(data).map(x => ({key: x[0], exp: x[1]}));
@ -179,11 +218,20 @@ export default {
if ( a_use && ! b_use ) return -1;
if ( ! a_use && b_use ) return 1;
if ( this.sort_by === 1 ) {
const a_r = a.exp.rarity,
b_r = b.exp.rarity;
if ( a_r < b_r ) return -1;
if ( a_r > b_r ) return 1;
}
const a_n = a.exp.name.toLowerCase(),
b_n = b.exp.name.toLowerCase();
if ( a_n < b_n ) return -1;
if ( a_n > b_n ) return 1;
return 0;
});
@ -204,6 +252,10 @@ export default {
exp.default = ! this.item.hasTwitchOverride(key);
},
onSort() {
this.sort_by = this.$refs.sort_select.selectedIndex;
},
onChange(event) {
const el = event.target,
idx = el.selectedIndex,
@ -223,8 +275,9 @@ export default {
key = el.dataset.key;
const exp = this.twitch_data[key],
offset = exp.in_use ? 0 : 1,
groups = exp && exp.groups,
entry = groups && groups[idx];
entry = groups && groups[idx - offset];
if ( entry )
this.item.setTwitchOverride(key, entry.value);
@ -235,6 +288,7 @@ export default {
if ( exp ) {
exp.value = value;
exp.default = ! this.item.hasOverride(key);
this.calculateRarity(exp);
}
},
@ -243,6 +297,7 @@ export default {
if ( exp ) {
exp.value = value;
exp.default = ! this.item.hasTwitchOverride(key);
this.calculateRarity(exp);
}
}
}

View file

@ -439,6 +439,11 @@ export default class Metadata extends Module {
el.disabled = maybe_call(def.disabled, this, data);
} catch(err) {
this.log.capture(err, {
tags: {
metadata: key
}
});
this.log.error(`Error rendering metadata for ${key}`, err);
return destroy();
}

174
src/raven.js Normal file
View file

@ -0,0 +1,174 @@
'use strict';
/* global FrankerFaceZ: false */
// ============================================================================
// Raven Logging
// ============================================================================
import {DEBUG, SENTRY_ID} from 'utilities/constants';
import {has} from 'utilities/object';
import Module from 'utilities/module';
import Raven from 'raven-js';
const BAD_URLS = [
'hls.ttvnw.net',
'trowel.twitch.tv',
'client-event-reporter.twitch.tv',
'.twitch.tv/gql',
'spade.twitch.tv'
];
const BAD_QUERIES = [
'ChannelPage_SetSessionStatus'
];
// ============================================================================
// Raven Logger
// ============================================================================
export default class RavenLogger extends Module {
constructor(...args) {
super(...args);
this.inject('settings');
this.inject('site');
this.inject('experiments');
this.raven = Raven;
Raven.config(SENTRY_ID, {
autoBreadcrumbs: {
console: false
},
release: FrankerFaceZ.version_info.toString(),
environment: DEBUG ? 'development' : 'production',
captureUnhandledRejections: false,
ignoreErrors: [
'InvalidAccessError',
'out of memory'
],
whitelistUrls: [
/cdn\.frankerfacez\.com/
],
sanitizeKeys: [
/Token$/
],
breadcrumbCallback(crumb) {
if ( crumb.category === 'gql' ) {
for(const matcher of BAD_QUERIES)
if ( crumb.message.includes(matcher) )
return false;
}
if ( crumb.type === 'http' ) {
const url = crumb.data.url;
for(const matcher of BAD_URLS)
if ( url.includes(matcher) )
return false;
}
return true;
},
shouldSendCallback(data) {
if ( data.message && data.messages.includes('raven-js/') )
return false;
return true;
}
}).install();
}
onEnable() {
const user = this.site.getUser();
if ( user )
this.raven.setUserContext({
id: user.id,
username: user.login
});
}
buildExtra() {
const context = this.settings.main_context,
chat_context = this.resolve('chat').context;
const settings = {},
chat_settings = {},
modules = {},
experiments = {},
twitch_experiments = {},
out = {
experiments,
twitch_experiments,
modules,
settings,
settings_context: context._context,
chat_settings,
chat_context: chat_context._context
};
for(const key in this.__modules)
if ( has(this.__modules, key) ) {
const mod = this.__modules[key];
modules[key] = [
mod.loaded ? 'loaded' : mod.loading ? 'loading' : 'unloaded',
mod.enabled ? 'enabled' : mod.enabling ? 'enabling' : 'disabled'
]
}
for(const [key, value] of context.__cache.entries())
settings[key] = value;
for(const [key, value] of chat_context.__cache.entries())
chat_settings[key] = value;
for(const [key, value] of Object.entries(this.experiments.getTwitchExperiments()))
if ( this.experiments.usingTwitchExperiment(key) )
twitch_experiments[value.name] = this.experiments.getTwitchAssignment(key);
for(const key of Object.keys(this.experiments.experiments))
experiments[key] = this.experiments.getAssignment(key);
return out;
}
buildTags() {
const core = this.site.getCore(),
out = {};
out.build = __webpack_hash__;
if ( core )
out.twitch_build = core.config.buildID;
return out;
}
addPlugin(...args) { return this.raven.addPlugin(...args) }
setUserContext(...args) { return this.raven.setUserContext(...args) }
captureException(exc, opts) {
opts = opts || {};
opts.extra = Object.assign(this.buildExtra(), opts.extra);
opts.tags = Object.assign(this.buildTags(), opts.tags);
return this.raven.captureException(exc, opts);
}
captureMessage(msg, opts) {
opts = opts || {};
opts.extra = Object.assign(this.buildExtra(), opts.extra);
opts.tags = Object.assign(this.buildTags(), opts.tags);
return this.raven.captureMessage(msg, opts);
}
captureBreadcrumb(...args) { return this.raven.captureBreadcrumb(...args) }
}

View file

@ -516,6 +516,7 @@ export default class ChatHook extends Module {
return ret;
} catch(err) {
t.log.capture(err, {extra: e});
return old_resub.call(i, e);
}
}
@ -533,6 +534,7 @@ export default class ChatHook extends Module {
return ret;
} catch(err) {
t.log.capture(err, {extra: e});
return old_ritual.call(i, e);
}
}

View file

@ -53,6 +53,9 @@ export default class RichContent extends Module {
}, data));
} catch(err) {
if ( err.message !== 'timeout' )
t.log.capture(err);
this.setState({
loaded: true,
error: true,

View file

@ -79,6 +79,8 @@ export default class Scroller extends Module {
ffz_errors: errs + 1,
ffz_total_errors: (this.state.ffz_total_errors||0) + 1
});
t.log.capture(err, {extra: info});
t.log.info('Error within Chat', err, info, errs);
}

View file

@ -1,3 +1,4 @@
.player .extension-overlay__iframe,
.player .extension-overlay {
pointer-events: none !important;
}

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]);

View file

@ -115,6 +115,10 @@
.ffz-i-lock:before { content: '\e820'; } /* '' */
.ffz-i-lock-open:before { content: '\e821'; } /* '' */
.ffz-i-arrows-cw:before { content: '\e822'; } /* '' */
.ffz-i-ignore:before { content: '\e823'; } /* '' */
.ffz-i-block:before { content: '\e824'; } /* '' */
.ffz-i-pin:before { content: '\e825'; } /* '' */
.ffz-i-pin-outline:before { content: '\e826'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */
.ffz-i-download-cloud:before { content: '\f0ed'; } /* '' */