2018-04-11 17:05:31 -04:00
'use strict' ;
/* global FrankerFaceZ: false */
// ============================================================================
// Raven Logging
// ============================================================================
2018-12-03 18:08:32 -05:00
import dayjs from 'dayjs' ;
2018-04-11 17:05:31 -04:00
import { DEBUG , SENTRY _ID } from 'utilities/constants' ;
import { has } from 'utilities/object' ;
import Module from 'utilities/module' ;
import Raven from 'raven-js' ;
2018-12-03 18:08:32 -05:00
const STRIP _URLS = /((?:\?|&)[^?&=]*?(?:oauth|token)[^?&=]*?=)[^?&]*?(&|$)/i ;
2018-04-19 16:40:01 -04:00
const AVALON _REG = /\/(?:script|static)\/((?:babel\/)?avalon)(\.js)(\?|#|$)/ ,
fix _url = url => url . replace ( AVALON _REG , ` /static/ $ 1. ${ _ _webpack _hash _ _ } $ 2 $ 3 ` ) ;
2018-04-16 18:56:42 -04:00
2018-04-11 17:05:31 -04:00
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'
] ;
2018-04-11 20:48:02 -04:00
const ERROR _TYPES = [
Error ,
TypeError ,
SyntaxError ,
ReferenceError
] ;
const ERROR _STRINGS = [
'the ducks are on fire' ,
"it's raining in shanghai" ,
'can we just not do this today?' ,
'cbenni likes butts' ,
'guys can you please not spam the errors my mom bought me this new error report server and it gets really hot when the errors are being spammed now my leg is starting to hurt because it is getting so hot'
] ;
2018-04-11 17:05:31 -04:00
// ============================================================================
// Raven Logger
// ============================================================================
export default class RavenLogger extends Module {
constructor ( ... args ) {
super ( ... args ) ;
this . inject ( 'settings' ) ;
this . inject ( 'site' ) ;
this . inject ( 'experiments' ) ;
2018-04-11 20:48:02 -04:00
// Do these in an event handler because we're initialized before
// settings are even ready.
this . once ( 'settings:enabled' , ( ) => {
this . settings . add ( 'reports.error.enable' , {
default : true ,
ui : {
path : 'Data Management > Reporting >> Error Reports' ,
title : 'Automatically send reports when an error occurs.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'reports.error.include-user' , {
2018-05-25 14:29:13 -04:00
default : false ,
2018-04-11 20:48:02 -04:00
ui : {
path : 'Data Management > Reporting >> Error Reports' ,
title : 'Include user IDs in reports.' ,
description : "Occasionally, it's useful to know which users are encountering issues so that we can check for specific badges, emote sets, or user flags that are causing issues." ,
component : 'setting-check-box'
}
} ) ;
this . settings . add ( 'reports.error.include-settings' , {
default : true ,
ui : {
path : 'Data Management > Reporting >> Error Reports' ,
title : 'Include a settings snapshot in reports.' ,
description : 'Knowing exactly what settings are in effect when an error happens can be incredibly useful for recreating the issue.' ,
component : 'setting-check-box'
}
} ) ;
this . settings . addUI ( 'reports.error.example' , {
2018-12-03 18:08:32 -05:00
path : 'Data Management > Reporting >> Example Report' ,
component : 'async-text' ,
2018-04-11 20:48:02 -04:00
watch : [
'reports.error.enable' ,
'reports.error.include-user' ,
'reports.error.include-settings'
] ,
data : ( ) => new Promise ( r => {
// Why fake an error when we can *make* an error?
2018-12-03 18:08:32 -05:00
this . _ _example _waiter = data => {
r ( JSON . stringify ( data , null , 4 ) ) ;
} ;
2018-04-11 20:48:02 -04:00
// Generate the error in a timeout so that the end user
// won't have a huge wall of a fake stack trace wasting
// their time.
const type = ERROR _TYPES [ Math . floor ( Math . random ( ) * ERROR _TYPES . length ) ] ,
msg = ERROR _STRINGS [ Math . floor ( Math . random ( ) * ERROR _STRINGS . length ) ] ;
setTimeout ( ( ) => this . log . capture ( new type ( msg ) , {
tags : {
example : true
}
} ) ) ;
} )
2018-04-15 17:19:22 -04:00
} ) ;
2018-04-11 20:48:02 -04:00
} ) ;
2018-04-11 17:05:31 -04:00
this . raven = Raven ;
2018-12-03 18:08:32 -05:00
const raven _config = {
2018-04-11 17:05:31 -04:00
autoBreadcrumbs : {
console : false
} ,
release : FrankerFaceZ . version _info . toString ( ) ,
environment : DEBUG ? 'development' : 'production' ,
captureUnhandledRejections : false ,
ignoreErrors : [
'InvalidAccessError' ,
2018-07-26 19:40:53 -04:00
'out of memory' ,
'Access is denied.' ,
2019-05-07 15:04:12 -04:00
'Zugriff verweigert' ,
'freed script' ,
'ffzenhancing'
2018-04-11 17:05:31 -04:00
] ,
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 ;
} ,
2018-04-11 20:48:02 -04:00
shouldSendCallback : data => {
if ( this . settings && ! this . settings . get ( 'reports.error.enable' ) ) {
2018-04-13 13:36:05 -04:00
if ( data . tags && data . tags . example && this . _ _example _waiter ) {
2018-04-11 20:48:02 -04:00
this . _ _example _waiter ( null ) ;
this . _ _example _waiter = null ;
}
return false ;
}
2018-04-13 13:36:05 -04:00
const exc = data . exception && data . exception . values [ 0 ] ;
2018-04-11 20:48:02 -04:00
// We don't want any of Sentry's junk.
2018-04-13 13:36:05 -04:00
if ( data . message && data . messages . includes ( 'raven-js/' ) || ( exc && JSON . stringify ( exc ) . includes ( 'raven-js/' ) ) )
2018-04-11 17:05:31 -04:00
return false ;
2018-04-11 20:48:02 -04:00
// We don't want any of Mozilla's junk either.
if ( exc && exc . type . startsWith ( 'NS_' ) )
return false ;
// Apparently, something is completely screwing up the DOM for
// at least two users? Not our problem.
if ( ! document . body || ! document . body . querySelector )
return false ;
if ( this . settings && this . settings . get ( 'reports.error.include-user' ) ) {
const user = this . site && this . site . getUser ( ) ;
if ( user )
data . user = { id : user . id , username : user . login }
}
data . extra = Object . assign ( this . buildExtra ( ) , data . extra ) ;
data . tags = Object . assign ( this . buildTags ( ) , data . tags ) ;
2018-04-19 16:40:01 -04:00
if ( data . exception && Array . isArray ( data . exception . values ) )
data . exception . values = this . rewriteStack ( data . exception . values , data ) ;
if ( data . culprit )
data . culprit = fix _url ( data . culprit ) ;
2018-04-11 20:48:02 -04:00
if ( data . tags . example ) {
if ( this . _ _example _waiter ) {
this . _ _example _waiter ( data ) ;
this . _ _example _waiter = null ;
}
return false ;
}
2018-12-04 04:20:00 -05:00
if ( DEBUG )
return false ;
2018-04-11 17:05:31 -04:00
return true ;
}
2018-12-03 18:08:32 -05:00
} ;
2018-12-04 04:25:01 -05:00
if ( ! DEBUG )
raven _config . whitelistUrls = [
/api\.frankerfacez\.com/ ,
/cdn\.frankerfacez\.com/
] ;
2018-12-03 18:08:32 -05:00
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' ) ;
2018-04-11 17:05:31 -04:00
}
2018-04-19 16:40:01 -04:00
2018-04-11 17:05:31 -04:00
onEnable ( ) {
2018-04-11 20:48:02 -04:00
this . log . info ( 'Installed error tracking.' ) ;
2018-04-11 17:05:31 -04:00
}
2018-04-16 18:56:42 -04:00
rewriteStack ( errors ) { // eslint-disable-line class-methods-use-this
for ( const err of errors ) {
if ( ! err || ! err . stacktrace || ! err . stacktrace . frames )
continue ;
for ( const frame of err . stacktrace . frames )
2018-04-19 16:40:01 -04:00
frame . filename = fix _url ( frame . filename ) ;
2018-04-16 18:56:42 -04:00
}
return errors ;
}
2018-04-11 17:05:31 -04:00
buildExtra ( ) {
2018-04-11 20:48:02 -04:00
const modules = { } ,
2018-04-11 17:05:31 -04:00
experiments = { } ,
twitch _experiments = { } ,
out = {
experiments ,
twitch _experiments ,
2018-04-11 20:48:02 -04:00
modules
2018-04-11 17:05:31 -04:00
} ;
for ( const key in this . _ _modules )
if ( has ( this . _ _modules , key ) ) {
const mod = this . _ _modules [ key ] ;
2018-04-11 20:48:02 -04:00
modules [ key ] = ` ${
mod . loaded ? 'loaded' : mod . loading ? 'loading' : 'unloaded' } $ {
mod . enabled ? 'enabled' : mod . enabling ? 'enabling' : 'disabled' } ` ;
2018-04-11 17:05:31 -04:00
}
2018-04-11 20:48:02 -04:00
if ( this . settings && this . settings . get ( 'reports.error.include-settings' ) ) {
const context = this . settings . main _context ,
chat = this . resolve ( 'chat' ) ,
chat _context = chat && chat . context ,
settings = out . settings = { } ,
chat _settings = out . chat _setting = chat _context ? { } : undefined ;
2018-04-11 17:05:31 -04:00
2018-04-11 20:48:02 -04:00
for ( const [ key , value ] of context . _ _cache . entries ( ) )
settings [ key ] = value ;
if ( chat _context )
for ( const [ key , value ] of chat _context . _ _cache . entries ( ) )
chat _settings [ key ] = value ;
}
2018-04-11 17:05:31 -04:00
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 = { } ;
2018-07-21 16:26:10 -04:00
out . flavor = this . site . constructor . name ;
2018-04-11 17:05:31 -04:00
out . build = _ _webpack _hash _ _ ;
2018-07-16 13:57:56 -04:00
out . git _commit = _ _git _commit _ _ ;
2018-04-11 17:05:31 -04:00
if ( core )
out . twitch _build = core . config . buildID ;
return out ;
}
addPlugin ( ... args ) { return this . raven . addPlugin ( ... args ) }
2018-04-11 20:48:02 -04:00
captureException ( exc , opts ) { return this . raven . captureException ( exc , opts ) }
captureMessage ( msg , opts ) { return this . raven . captureMessage ( msg , opts ) }
2018-04-11 17:05:31 -04:00
captureBreadcrumb ( ... args ) { return this . raven . captureBreadcrumb ( ... args ) }
}