1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-05 18:48:31 +00:00

4.0.0-rc13.11

* Added: Debug Log generation and easy uploading for issue reports.
* Fixed: Bug with chat lines not having a state causing rendering to crash.
* Fixed: Add an error boundary around the custom FFZ Emote Menu to catch errors, displaying feedback and sending error reports rather than silently failing.
* Fixed: Typo in Fine when logging errors.
* Changed: Update chat types with the latest values from Twitch's source.
* Changed: Update the GitHub issue template with instructions on uploading logs from v4.
This commit is contained in:
SirStendec 2018-12-03 18:08:32 -05:00
parent cb2f2b19ee
commit d6504757b3
18 changed files with 413 additions and 99 deletions

View file

@ -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**:

View file

@ -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() {

View file

@ -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.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: () =>

View file

@ -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' ) {

View file

@ -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) )

View file

@ -0,0 +1,145 @@
<template lang="html">
<div class="ffz--example-report tw-relative ">
<div v-if="canUpload" class="tw-absolute ffz--report-upload">
<div v-if="uploading">
<button
class="tw-button tw-button--disabled"
disabled
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-upload-cloud" />
</span>
<span class="tw-button__text">
{{ t('async-text.uploading', 'Uploading...') }}
</span>
</button>
</div>
<div v-else-if="url">
<input
ref="url_box"
:value="url"
class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input"
type="text"
readonly
@focusin="selectURL"
>
</div>
<div v-else>
<button
class="tw-button"
@click="upload"
>
<span class="tw-button__icon tw-button__icon--left">
<figure class="ffz-i-upload-cloud" />
</span>
<span class="tw-button__text">
{{ t('async-text.upload', 'Upload') }}
</span>
</button>
</div>
</div>
<div class="tw-c-background-alt-2 tw-font-size-5 tw-pd-y-05 tw-pd-x-1 tw-border-radius-large">
<div v-if="loading" class="tw-align-center">
<h1 class="tw-mg-5 ffz-i-zreknarf loading" />
</div>
<code v-else>{{ text }}</code>
</div>
</div>
</template>
<script>
export default {
props: ['item', 'context'],
data() {
return {
uploading: false,
url: null,
loading: false,
text: null
}
},
computed: {
canUpload() {
return ! this.loading && this.text;
}
},
created() {
this.refresh();
const ctx = this.context.context;
for(const key of this.item.watch)
ctx.on(`changed:${key}`, this.refresh, this);
},
destroyed() {
const ctx = this.context.context;
for(const key of this.item.watch)
ctx.off(`changed:${key}`, this.refresh, this);
},
methods: {
selectURL() {
if ( this.$refs && this.$refs.url_box )
this.$refs.url_box.select();
},
async upload() {
if ( this.uploading || this.url || this.text == null )
return;
this.uploading = true;
const response = await fetch('https://putco.de', {
method: 'PUT',
body: this.text
});
if ( ! response.ok ) {
this.uploading = false;
this.url = 'An error occured.';
}
this.url = await response.text();
if ( this.url.startsWith('http://') )
this.url = `https://${this.url.slice(7)}`;
this.uploading = false;
},
async refresh() {
this.uploading = false;
this.url = null;
this.loading = true;
this.text = await this.item.data();
this.loading = false;
}
}
}
</script>
<style lang="scss" scoped>
.ffz--example-report {
div {
max-height: 30rem;
overflow-y: auto;
code {
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
}
}
}
.ffz--report-upload {
z-index: 1;
position: absolute;
top: 1rem;
right: 3rem;
}
</style>

View file

@ -1,58 +0,0 @@
<template lang="html">
<div class="ffz--example-report">
<h4 class="tw-mg-b-05">{{ t('reports.example', 'Example Report') }}</h4>
<div class="tw-c-background-alt-2 tw-font-size-5 tw-pd-y-05 tw-pd-x-1 tw-border-radius-large">
<code>{{ JSON.stringify(example, null, 4) }}</code>
</div>
</div>
</template>
<script>
export default {
props: ['item', 'context'],
data() {
return {
example: null
}
},
created() {
this.refresh();
const ctx = this.context.context;
for(const key of this.item.watch)
ctx.on(`changed:${key}`, this.refresh, this);
},
destroyed() {
const ctx = this.context.context;
for(const key of this.item.watch)
ctx.off(`changed:${key}`, this.refresh, this);
},
methods: {
refresh() {
this.example = 'Loading...';
this.item.data().then(data => this.example = data);
}
}
}
</script>
<style lang="scss" scoped>
.ffz--example-report {
div {
max-height: 30rem;
overflow-y: auto;
code {
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
}
}
}
</style>

View file

@ -1,15 +1,9 @@
<template lang="html">
<div class="ffz--home tw-border-t tw-pd-t-1">
<div class="ffz--home tw-border-t tw-pd-y-1">
<h2>Feedback</h2>
<div class="tw-mg-y-1 tw-c-background-accent tw-c-text-overlay tw-pd-1">
<h3 class="ffz-i-attention">
Please keep in mind that FrankerFaceZ v4 is under heavy development.
</h3>
</div>
<p>
Okay, still here? Great! You can provide feedback and bug reports by
You can provide feedback and bug reports by
<a href="https://github.com/FrankerFaceZ/FrankerFaceZ/issues" target="_blank" rel="noopener">
opening an issue at our GitHub repository</a>.
@ -21,6 +15,11 @@
When creating a GitHub issue, please check that someone else hasn't
already created one for what you'd like to discuss or report.
</p>
<p>
When creating an issue, please also upload the following logs and
include a link in your report.
</p>
</div>
</template>

View file

@ -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'

View file

@ -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 )

View file

@ -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');
}

View file

@ -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.
*/

View file

@ -277,7 +277,8 @@ 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 (<t.MenuComponent
return (<t.MenuErrorWrapper visible={this.props.visible}>
<t.MenuComponent
visible={this.props.visible}
toggleVisibility={this.props.toggleVisibility}
onClickEmote={this.props.onClickEmote}
@ -287,7 +288,8 @@ export default class EmoteMenu extends Module {
channel_id={this.props.channelOwnerID}
loading={this.state.gqlLoading}
error={this.state.gqlError}
/>)
/>
</t.MenuErrorWrapper>)
}
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 (<div
class={`tw-balloon tw-balloon--md tw-balloon--up tw-balloon--right tw-block tw-absolute ffz--emote-picker${padding ? ' reduced-padding' : ''}`}
data-a-target="emote-picker"
>
<div class="tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base">
<div
class="emote-picker__tab-content scrollable-area"
data-test-selector="scrollable-area-wrapper"
data-simplebar
>
<div class="tw-align-center tw-pd-1">
<div class="tw-mg-b-1">
<div class="tw-mg-2">
<img
src="//cdn.frankerfacez.com/emoticon/26608/2"
srcSet="//cdn.frankerfacez.com/emoticon/26608/2 1x, //cdn.frankerfacez.com/emoticon/26608/4 2x"
/>
</div>
{t.i18n.t('emote-menu.error', 'There was an error rendering this menu.')}
<br />
{t.settings.get('reports.error.enable') ?
t.i18n.t('emote-menu.error-report', 'An error report has been automatically submitted.')
: ''
}
<div class="tw-mg-t-05 tw-border-t-1 tw-pd-t-05">
{t.i18n.t('emote-menu.disable', 'As a temporary workaround, try disabling the FFZ Emote Menu in the FFZ Control Center.') }
</div>
</div>
</div>
</div>
</div>
</div>);
}
return this.props.children;
}
}
this.MenuComponent = class FFZEmoteMenuComponent extends React.Component {
constructor(props) {
super(props);

View file

@ -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 )

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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,12 +166,14 @@ export default class Dialog extends EventEmitter {
if ( container === old_container )
return;
if ( maximized )
if ( maximized ) {
if ( container )
container.classList.add('ffz-has-dialog');
else
} else if ( old_container )
old_container.classList.remove('ffz-has-dialog');
this._element.remove();
if ( container )
container.appendChild(this._element);
this.emit('resize');

View file

@ -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,