diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 87c024dd..7429fd0f 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -2,7 +2,7 @@ **Do you use BetterTTV or other Twitch extensions**: -**FFZ Logs (via FFZ Menu > About > Logs; if Applicable)**: +**FFZ Logs (via FFZ Control Center > Home > Feedback >> Log; if Applicable)**: **Bug / Idea**: diff --git a/src/experiments.js b/src/experiments.js index 6838e31e..3c5471ca 100644 --- a/src/experiments.js +++ b/src/experiments.js @@ -123,6 +123,25 @@ export default class ExperimentManager extends Module { } + generateLog() { + const out = [ + `Unique ID: ${this.unique_id}`, + '' + ]; + + for(const [key, value] of Object.entries(this.experiments)) { + out.push(`FFZ | ${value.name}: ${this.getAssignment(key)}${this.hasOverride(key) ? ' (Overriden)' : ''}`); + } + + for(const [key, value] of Object.entries(this.getTwitchExperiments())) { + if ( this.usingTwitchExperiment(key) ) + out.push(`TWITCH | ${value.name}: ${this.getTwitchAssignment(key)}${this.hasTwitchOverride(key) ? ' (Overriden)' : ''}`) + } + + return out.join('\n'); + } + + // Twitch Experiments getTwitchExperiments() { diff --git a/src/main.js b/src/main.js index 975e3c97..3c2c2e9a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ 'use strict'; +import dayjs from 'dayjs'; import RavenLogger from './raven'; import Logger from 'utilities/logging'; @@ -13,6 +14,8 @@ import {TranslationManager} from './i18n'; import SocketClient from './socket'; import Site from 'site'; import Vue from 'utilities/vue'; +import { timeout } from './utilities/object'; +import { strict } from 'assert'; class FrankerFaceZ extends Module { constructor() { @@ -30,10 +33,11 @@ class FrankerFaceZ extends Module { // Error Reporting and Logging // ======================================================================== - if ( ! DEBUG ) - this.inject('raven', RavenLogger); + this.inject('raven', RavenLogger); this.log = new Logger(null, null, null, this.raven); + this.log.init = true; + this.core_log = this.log.get('core'); this.log.info(`FrankerFaceZ v${VER} (build ${VER.build}${VER.commit ? ` - commit ${VER.commit}` : ''})`); @@ -61,9 +65,11 @@ class FrankerFaceZ extends Module { this.enable().then(() => this.enableInitialModules()).then(() => { const duration = performance.now() - start_time; this.core_log.info(`Initialization complete in ${duration.toFixed(5)}ms.`); + this.log.init = false; }).catch(err => { this.core_log.error('An error occurred during initialization.', err); + this.log.init = false; }); } @@ -72,6 +78,50 @@ class FrankerFaceZ extends Module { } + // ======================================================================== + // Generate Log + // ======================================================================== + + async generateLog() { + const promises = []; + for(const key in this.__modules) { + const module = this.__modules[key]; + if ( module instanceof Module && module.generateLog && module != this ) + promises.push((async () => { + try { + return [ + key, + await timeout(Promise.resolve(module.generateLog()), 5000) + ]; + } catch(err) { + return [ + key, + `Error: ${err}` + ] + } + })()); + } + + const out = await Promise.all(promises); + + if ( this.log.captured_init && this.log.captured_init.length > 0 ) { + const logs = []; + for(const msg of this.log.captured_init) { + const time = dayjs(msg.time).locale('en').format('H:mm:ss'); + logs.push(`[${time}] ${msg.level} | ${msg.category || 'core'}: ${msg.message}`); + } + + out.unshift(['initialization', logs.join('\n')]); + } + + return out.map(x => { + return `${x[0]} +------------------------------------------------------------------------------- +${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}` + }).join('\n\n'); + } + + // ======================================================================== // Modules // ======================================================================== @@ -100,7 +150,7 @@ class FrankerFaceZ extends Module { FrankerFaceZ.Logger = Logger; const VER = FrankerFaceZ.version_info = { - major: 4, minor: 0, revision: 0, extra: '-rc13.10', + major: 4, minor: 0, revision: 0, extra: '-rc13.11', commit: __git_commit__, build: __webpack_hash__, toString: () => diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index 01b7b98b..40efdd4e 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -267,6 +267,9 @@ export default class Badges extends Module { data = JSON.parse(target.dataset.badgeData), out = []; + if ( data == null ) + return out; + for(const d of data) { const p = d.provider; if ( p === 'twitch' ) { diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 33d9f962..0a5aed16 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -3,6 +3,7 @@ // ============================================================================ // Chat // ============================================================================ + import dayjs from 'dayjs'; import Module from 'utilities/module'; @@ -559,6 +560,15 @@ export default class Chat extends Module { } + generateLog() { + const out = ['chat settings', '-------------------------------------------------------------------------------']; + for(const [key, value] of this.context.__cache.entries()) + out.push(`${key}: ${JSON.stringify(value)}`); + + return out.join('\n'); + } + + onEnable() { for(const key in TOKENIZERS) if ( has(TOKENIZERS, key) ) diff --git a/src/modules/main_menu/components/async-text.vue b/src/modules/main_menu/components/async-text.vue new file mode 100644 index 00000000..ec992197 --- /dev/null +++ b/src/modules/main_menu/components/async-text.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/modules/main_menu/components/example-report.vue b/src/modules/main_menu/components/example-report.vue deleted file mode 100644 index 391f51d7..00000000 --- a/src/modules/main_menu/components/example-report.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - diff --git a/src/modules/main_menu/components/feedback-page.vue b/src/modules/main_menu/components/feedback-page.vue index a3ab7a46..3f8a4d41 100644 --- a/src/modules/main_menu/components/feedback-page.vue +++ b/src/modules/main_menu/components/feedback-page.vue @@ -1,15 +1,9 @@ diff --git a/src/modules/main_menu/index.js b/src/modules/main_menu/index.js index 23ce2acd..fa138244 100644 --- a/src/modules/main_menu/index.js +++ b/src/modules/main_menu/index.js @@ -57,6 +57,16 @@ export default class MainMenu extends Module { component: 'feedback-page' }); + this.settings.addUI('feedback.log', { + path: 'Home > Feedback >> Log @{"sort": 1000}', + component: 'async-text', + watch: [ + 'reports.error.include-user', + 'reports.error.include-settings' + ], + data: () => this.resolve('core').generateLog() + }) + this.settings.addUI('changelog', { path: 'Home > Changelog', component: 'changelog' diff --git a/src/modules/metadata.jsx b/src/modules/metadata.jsx index a928df7d..97e8fefd 100644 --- a/src/modules/metadata.jsx +++ b/src/modules/metadata.jsx @@ -832,6 +832,9 @@ export default class Metadata extends Module { } else { stat = el.querySelector('.ffz-stat-text'); + if ( ! stat ) + return destroy(); + old_color = el.dataset.color || ''; if ( el._ffz_order !== order ) diff --git a/src/raven.js b/src/raven.js index 5725478f..f21e407d 100644 --- a/src/raven.js +++ b/src/raven.js @@ -6,12 +6,16 @@ // Raven Logging // ============================================================================ +import dayjs from 'dayjs'; + import {DEBUG, SENTRY_ID} from 'utilities/constants'; import {has} from 'utilities/object'; import Module from 'utilities/module'; import Raven from 'raven-js'; +const STRIP_URLS = /((?:\?|&)[^?&=]*?(?:oauth|token)[^?&=]*?=)[^?&]*?(&|$)/i; + const AVALON_REG = /\/(?:script|static)\/((?:babel\/)?avalon)(\.js)(\?|#|$)/, fix_url = url => url.replace(AVALON_REG, `/static/$1.${__webpack_hash__}$2$3`); @@ -90,8 +94,8 @@ export default class RavenLogger extends Module { }); this.settings.addUI('reports.error.example', { - path: 'Data Management > Reporting >> Error Reports', - component: 'example-report', + path: 'Data Management > Reporting >> Example Report', + component: 'async-text', watch: [ 'reports.error.enable', @@ -101,7 +105,9 @@ export default class RavenLogger extends Module { data: () => new Promise(r => { // Why fake an error when we can *make* an error? - this.__example_waiter = r; + this.__example_waiter = data => { + r(JSON.stringify(data, null, 4)); + }; // Generate the error in a timeout so that the end user // won't have a huge wall of a fake stack trace wasting @@ -121,7 +127,7 @@ export default class RavenLogger extends Module { this.raven = Raven; - Raven.config(SENTRY_ID, { + const raven_config = { autoBreadcrumbs: { console: false }, @@ -134,9 +140,6 @@ export default class RavenLogger extends Module { 'Access is denied.', 'Zugriff verweigert' ], - whitelistUrls: [ - /cdn\.frankerfacez\.com/ - ], sanitizeKeys: [ /Token$/ ], @@ -166,6 +169,10 @@ export default class RavenLogger extends Module { return false; } + // Don't send errors in debug mode. + //if ( DEBUG && !(data.tags && data.tags.example) ) + // return false; + const exc = data.exception && data.exception.values[0]; // We don't want any of Sentry's junk. @@ -207,7 +214,38 @@ export default class RavenLogger extends Module { return true; } - }).install(); + }; + + if ( ! DEBUG ) + raven_config.whitelistUrls = [ + /cdn\.frankerfacez\.com/ + ]; + + Raven.config(SENTRY_ID, raven_config).install(); + } + + + generateLog() { + if ( ! this.raven || ! this.raven._breadcrumbs ) + return 'No breadcrumbs to log.'; + + return this.raven._breadcrumbs.map(crumb => { + const time = dayjs(crumb.timestamp).locale('en').format('H:mm:ss'); + if ( crumb.type == 'http' ) + return `[${time}] HTTP | ${crumb.category}: ${crumb.data.method} ${crumb.data.url} -> ${crumb.data.status_code}`; + + let cat = 'LOG'; + if ( crumb.category && crumb.category.includes('ui.') ) + cat = 'UI'; + + return `[${time}] ${cat}${crumb.level ? `:${crumb.level}` : ''} | ${crumb.category}: ${crumb.message}${crumb.data ? `\n ${JSON.stringify(crumb.data)}` : ''}`; + + }).map(x => { + if ( typeof x !== 'string' ) + x = `${x}`; + + return x.replace(STRIP_URLS, '$1REDACTED$2'); + }).join('\n'); } diff --git a/src/settings/index.js b/src/settings/index.js index 5e152ec2..c786b4cf 100644 --- a/src/settings/index.js +++ b/src/settings/index.js @@ -64,6 +64,14 @@ export default class SettingsManager extends Module { this.enable(); } + generateLog() { + const out = []; + for(const [key, value] of this.main_context.__cache.entries()) + out.push(`${key}: ${JSON.stringify(value)}`); + + return out.join('\n'); + } + /** * Called when the SettingsManager instance should be enabled. */ diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index 898bb1ab..c295d77e 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -277,17 +277,19 @@ export default class EmoteMenu extends Module { if ( ! this.props || ! has(this.props, 'channelOwnerID') || ! t.chat.context.get('chat.emote-menu.enabled') ) return old_render.call(this); - return () + return ( + + ) } this.EmoteMenu.forceUpdate(); @@ -731,6 +733,73 @@ export default class EmoteMenu extends Module { setTimeout(doClear, 100); }; + this.MenuErrorWrapper = class FFZEmoteMenuErrorWrapper extends React.Component { + constructor(props) { + super(props); + this.state = {errored: false, error: null}; + } + + static getDerivedStateFromError(error) { + return { + errored: true, + error + } + } + + componentDidCatch(error) { // eslint-disable-line class-methods-use-this + t.log.capture(error); + t.log.error('Error rendering the FFZ Emote Menu.'); + this.setState({ + errored: true, + error + }); + } + + render() { + if ( this.state.errored ) { + if ( ! this.props.visible ) + return null; + + const padding = t.chat.context.get('chat.emote-menu.reduced-padding'); + + return (
+
+
+
+
+
+ +
+ {t.i18n.t('emote-menu.error', 'There was an error rendering this menu.')} +
+ {t.settings.get('reports.error.enable') ? + t.i18n.t('emote-menu.error-report', 'An error report has been automatically submitted.') + : '' + } +
+ {t.i18n.t('emote-menu.disable', 'As a temporary workaround, try disabling the FFZ Emote Menu in the FFZ Control Center.') } +
+
+
+
+
+
); + } + + return this.props.children; + } + } + this.MenuComponent = class FFZEmoteMenuComponent extends React.Component { constructor(props) { super(props); diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 34ec6c8e..bd9cecc9 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -105,7 +105,8 @@ const CHAT_TYPES = make_enum( 'CrateGift', 'RewardGift', 'SubMysteryGift', - 'AnonSubMysteryGift' + 'AnonSubMysteryGift', + 'FirstCheerMessage' ); @@ -268,7 +269,6 @@ export default class ChatHook extends Module { }); } - get currentChat() { for(const inst of this.ChatController.instances) if ( inst && inst.chatService ) diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 4b01ba06..d57b8579 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -209,7 +209,7 @@ export default class ChatLine extends Module { const old_render = cls.prototype.render; cls.prototype.shouldComponentUpdate = function(props, state) { - const show = state.alwaysShowMessage || ! props.message.deleted, + const show = state && state.alwaysShowMessage || ! props.message.deleted, old_show = this._ffz_show; // We can't just compare props.message.deleted to this.props.message.deleted @@ -256,7 +256,7 @@ export default class ChatLine extends Module { show = true; show_class = msg.deleted; } else { - show = this.state.alwaysShowMessage || ! msg.deleted; + show = this.state && this.state.alwaysShowMessage || ! msg.deleted; show_class = false; } diff --git a/src/utilities/compat/fine.js b/src/utilities/compat/fine.js index a9fdf4b2..0a22ed89 100644 --- a/src/utilities/compat/fine.js +++ b/src/utilities/compat/fine.js @@ -619,7 +619,7 @@ export class FineWrapper extends EventEmitter { } }); - this.finelog.error(`An error occured when calling forceUpdate on an instance of ${this.name}`, err); + this.fine.log.error(`An error occurred when calling forceUpdate on an instance of ${this.name}`, err); } } diff --git a/src/utilities/dialog.js b/src/utilities/dialog.js index dc20e762..4071717b 100644 --- a/src/utilities/dialog.js +++ b/src/utilities/dialog.js @@ -116,7 +116,7 @@ export default class Dialog extends EventEmitter { visible = this._visible = ! this._visible, container = this.getContainer(); - if ( maximized ) + if ( maximized && container ) container.classList.toggle('ffz-has-dialog', visible); if ( ! visible ) { @@ -129,6 +129,9 @@ export default class Dialog extends EventEmitter { return; } + if ( ! container ) + return; + if ( this.factory ) { const el = this.factory(); if ( el instanceof Promise ) { @@ -163,13 +166,15 @@ export default class Dialog extends EventEmitter { if ( container === old_container ) return; - if ( maximized ) - container.classList.add('ffz-has-dialog'); - else + if ( maximized ) { + if ( container ) + container.classList.add('ffz-has-dialog'); + } else if ( old_container ) old_container.classList.remove('ffz-has-dialog'); this._element.remove(); - container.appendChild(this._element); + if ( container ) + container.appendChild(this._element); this.emit('resize'); } diff --git a/src/utilities/logging.js b/src/utilities/logging.js index 95fbc372..72d82ea2 100644 --- a/src/utilities/logging.js +++ b/src/utilities/logging.js @@ -10,9 +10,14 @@ const RAVEN_LEVELS = { export default class Logger { constructor(parent, name, level, raven) { + this.root = parent ? parent.root : this; this.parent = parent; this.name = name; + if ( this.root == this ) + this.captured_init = []; + + this.init = false; this.enabled = true; this.level = level || (parent && parent.level) || Logger.DEFAULT_LEVEL; this.raven = raven || (parent && parent.raven); @@ -68,6 +73,14 @@ export default class Logger { const message = Array.prototype.slice.call(args); + if ( this.root.init ) + this.root.captured_init.push({ + time: Date.now(), + category: this.name, + message: message.join(' '), + level: RAVEN_LEVELS[level] || level + }); + this.crumb({ message: message.join(' '), category: this.name,