Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

[Scripter's Tool] True console logging for API development

1651650984

Edited 1651653702
Oosh
Sheet Author
API Scripter
This is a script for firemonkey to pass API logging through to the browser console, where it can be logged properly. It also passes through to the Campaign tab console, so you can get your API logs in the same tab as the game, and with objects logged properly to allow drilling down. I haven't tested the script with violent/tamper/othermonkeys, it might need some header tweaks, or it might just work. Not sure. If you do want a hand getting it to work for a different monkey, let me know. The GM API is a real pig, to be honest. I don't have much good to say about GM, or about firemonkey. Writing an actual extension is much easier than dealing with a mangled set of API accessors. BAD MONKEY! There are two parts to the tool: &nbsp;&nbsp;&nbsp; 1) the firemonkey script - this will enable relogging of all API log() output to the API tab and the Campaign tab. Note that these must be in the same &nbsp;browser instance, userscripts do not have access to other browser windows. &nbsp;&nbsp;&nbsp; 2) an optional API construct to pass through regular console functions, enabling console.log(), console.info(), console.warn() and console.error(). console.error will also pass through a current API stack trace. There's also a variable called debugLevel, allowing you to disable logging of less serious log types - e.g. setting it to 2 will only log console.error() and console.warn() events. Setting it to 0 will disable all console() logs. The API's own log() function is not affected by any of this - these will always be passed out as console.log() to the browser. This also allows logging more than one parameter, however since it still needs to pass through the API/Ace's log() function, the parameters will come out in the browser as separate lines. Moneyshot: There's some settings at the top of the script that can be tweaked: &nbsp;&nbsp;&nbsp; bufferSize : set this higher if you have a lot of logs coming out very rapidly (or you notice any are missing) &nbsp;&nbsp;&nbsp; bufferDecay : set this higher if you're getting duplicate logs &nbsp;&nbsp;&nbsp; campaignPassthrough : set to false to disable passing the logs to the campaign tab &nbsp;&nbsp;&nbsp; prefixText/prefixStyle : the styled prefix for each log in the browser window to denote API logs &nbsp;&nbsp;&nbsp;&nbsp; charMap : edit the control characters used to pass through info/warn/error. Changes here will need to be mirrored in the API construct. The monkey script: Important: this must be 'enabled' in FM to function. Running a script manually in firemonkey places it in a different context to an enabled script, where, frustratingly, it doesn't have access to the GM API. There is another method using sessionStorage listeners that works for a manual script, if anyone needs. // ==UserScript== // @name apiconsole-relogger // @match <a href="https://app.roll20.net/*" rel="nofollow">https://app.roll20.net/*</a> // @version 1.0 // ==/UserScript== /* globals GM */ ( async () =&gt; { &nbsp; &nbsp; // Settings to modify if required &nbsp; const bufferSize = 30 ; &nbsp; const bufferDecay = 100 ; &nbsp; const campaignPassthrough = true ; &nbsp; const prefixStyle = `color:cyan; border:1px blue solid; padding: 0px 3px 0px 3px; border radius: 2px; background-color: #444444` ; &nbsp; const prefixText = `API` ; &nbsp; const charMap = { &nbsp; &nbsp; //log: ``, &nbsp; &nbsp; info : `·` , &nbsp; &nbsp; warn : `°` , &nbsp; &nbsp; error : `¡` , &nbsp; }; &nbsp; &nbsp; &nbsp; const location = window . location . href ; &nbsp; let bufferIndex = 0 ; &nbsp; const outputLog = ( newLog ) =&gt; { &nbsp; &nbsp; if ( newLog . length &lt; 3 ) return ; &nbsp; &nbsp; const firstChar = newLog [ 2 ][ 0 ]; &nbsp; &nbsp; if ( newLog [ 2 ]. stack ) { &nbsp; &nbsp; &nbsp; const style = `color: pink; font-weight: bold; background: #4e2c34; padding: 0px 6px 0px 6px; border: 1px solid darkred; border-radius: 2px;` ; &nbsp; &nbsp; &nbsp; console . groupCollapsed ( `%cAPI stack.trace` , style ); &nbsp; &nbsp; &nbsp; console . error ( newLog [ 2 ]. stack ); &nbsp; &nbsp; &nbsp; console . groupEnd (); &nbsp; &nbsp; } &nbsp; &nbsp; else if ( Object . values ( charMap ). includes ( firstChar )) { &nbsp; &nbsp; &nbsp; newLog [ 2 ] = newLog [ 2 ]. slice ( 1 ); &nbsp; &nbsp; &nbsp; if ( firstChar === charMap . info ) console . info (... newLog ); &nbsp; &nbsp; &nbsp; else if ( firstChar === charMap . warn ) console . warn (... newLog ); &nbsp; &nbsp; &nbsp; else if ( firstChar === charMap . error ) console . error (... newLog ); &nbsp; &nbsp; } &nbsp; &nbsp; else console . log (... newLog ); &nbsp; } &nbsp; &nbsp; // Emitter path &nbsp; if ( /campaigns \/ scripts/ i . test ( location )) { &nbsp; &nbsp; const passthroughLog = ( log ) =&gt; { &nbsp; &nbsp; &nbsp; const logKey = `newlog ${ bufferIndex } ` ; &nbsp; &nbsp; &nbsp; bufferIndex = ( bufferIndex + 1 ) % bufferSize ; &nbsp; &nbsp; &nbsp; GM . setValue ( logKey , JSON . stringify ( log )); &nbsp; &nbsp; } &nbsp; &nbsp; const setupEventsElement = ( elementName ) =&gt; { &nbsp; &nbsp; &nbsp; const eventElement = document . createElement ( 'div' ); &nbsp; &nbsp; &nbsp; eventElement . style . display = 'none' ; &nbsp; &nbsp; &nbsp; eventElement . id = elementName ; &nbsp; &nbsp; &nbsp; document . body . append ( eventElement ); &nbsp; &nbsp; &nbsp; return eventElement ; &nbsp; &nbsp; } &nbsp; &nbsp; const reloggerEvents = setupEventsElement ( 'relogger-events' ); &nbsp; &nbsp; reloggerEvents . addEventListener ( 'apilog' , ( ev ) =&gt; { &nbsp; &nbsp; &nbsp; outputLog ( ev . detail ); &nbsp; &nbsp; &nbsp; if ( campaignPassthrough ) passthroughLog ( ev . detail ); &nbsp; &nbsp; }); &nbsp; &nbsp; const contentScript = ` &nbsp; &nbsp; &nbsp; (() =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; const editSession = window.ace.edit('apiconsole'); &nbsp; &nbsp; &nbsp; &nbsp; editSession.$blockScrolling = 'Infinity'; &nbsp; &nbsp; &nbsp; &nbsp; editSession.session.on('change', (ev) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; let content = ev.lines.filter(v=&gt;v); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content = content.reduce((acc, line, i) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (!line.trim()) return acc; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; acc += (i&gt;0) ? (' \\\\ n'+line) : line; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return acc; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }, ''); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; try { content = JSON.parse(content) } catch(e) { e } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const output = ['%c ${ prefixText } ', ' ${ prefixStyle } ', content]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; document.querySelector('#relogger-events').dispatchEvent(new CustomEvent('apilog', { detail: output })); &nbsp; &nbsp; &nbsp; &nbsp; }); &nbsp; &nbsp; &nbsp; })(); &nbsp; &nbsp; ` ; &nbsp; &nbsp; const injectContentScript = ( scriptString ) =&gt; { &nbsp; &nbsp; &nbsp; const newScript = document . createElement ( 'script' ); &nbsp; &nbsp; &nbsp; newScript . type = `text/javascript` ; &nbsp; &nbsp; &nbsp; newScript . innerText = scriptString ; &nbsp; &nbsp; &nbsp; document . head . append ( newScript ); &nbsp; &nbsp; } &nbsp; &nbsp; injectContentScript ( contentScript ); &nbsp; } &nbsp; // Receiver path &nbsp; else if ( /net \/ editor/ . test ( location )) { &nbsp; &nbsp; const busy = Array ( bufferSize ). fill ( false ); &nbsp; &nbsp; await new Promise ( res =&gt; setTimeout (() =&gt; res (), 7000 )); &nbsp; &nbsp; const decayChannel = async ( channel ) =&gt; { setTimeout (() =&gt; busy [ channel ] = false , bufferDecay ) } &nbsp; &nbsp; const logListener = ( index , log ) =&gt; { &nbsp; &nbsp; &nbsp; if ( busy [ index ]) return ; &nbsp; &nbsp; &nbsp; busy [ index ] = true ; &nbsp; &nbsp; &nbsp; let logArray ; &nbsp; &nbsp; &nbsp; try { logArray = JSON . parse ( log ) } catch ( e ) { &nbsp;} &nbsp; &nbsp; &nbsp; if ( logArray ) outputLog ( logArray ); &nbsp; &nbsp; &nbsp; decayChannel ( index ); &nbsp; &nbsp; } &nbsp; &nbsp; for ( let i = 0 ; i &lt; bufferSize ; i ++) { GM . addValueChangeListener ( `newlog ${ i } ` , ( channel , old , log ) =&gt; logListener ( i , ` ${ log } ` )) } &nbsp; &nbsp; console . info ( '%c=== API listener online ===' , prefixStyle ); &nbsp; } })(); And the API construct to pass console() functions out. Throw it somewhere out of the way in your script, or if you really like it, just make it its own script that others can access. Don't wait for the ready event to declare it, or you'll miss any console.logging from scripts before it. If you really really like it, you could write a mini-handleInput to allow changing debugLevel from in campaign. That would allow on-the-fly, sandbox-wide debug control without the need to comment stuff out or edit individual scripts (though they would all need to be written using console logging instead of API log() ). let debugLevel = 4 ; const console = (() =&gt; { &nbsp; const charMap = { &nbsp; &nbsp; &nbsp; // log: ``, &nbsp; &nbsp; &nbsp; info : `·` , &nbsp; &nbsp; &nbsp; warn : `°` , &nbsp; &nbsp; &nbsp; error : `¡` , &nbsp; } &nbsp; &nbsp; &nbsp; const passthrough = (... args ) =&gt; args . forEach ( a =&gt; log ( a )); &nbsp; const nope = () =&gt; {}; &nbsp; return { &nbsp; &nbsp; log : ( log , ... args ) =&gt; debugLevel &gt; 3 ? passthrough ( ` ${ charMap . log }${ log } ` , ... args ) : nope , &nbsp; &nbsp; info : ( log , ... args ) =&gt; debugLevel &gt; 2 ? passthrough ( ` ${ charMap . info }${ log } ` , ... args ) : nope , &nbsp; &nbsp; warn : ( log , ... args ) =&gt; debugLevel &gt; 1 ? passthrough ( ` ${ charMap . warn }${ log } ` , ... args ) : nope , &nbsp; &nbsp; error : ( log , ... args ) =&gt; debugLevel &gt; 0 ? passthrough ( ` ${ charMap . error }${ log } ` , ... args , { stack : ( new Error ( 'API stack' )). stack }) : nope , &nbsp; } })();
1651656715

Edited 1651682232
Trying to understand what this solution does to achieve a single console.log with included api logging. In a single browser two tabs are opened. One tab with <a href="https://app.roll20.net/campaigns/scripts/nnn" rel="nofollow">https://app.roll20.net/campaigns/scripts/nnn</a> and one tab with the VTT campaign editor. Firemonkey injects code in both pages and then that code passes the logging from the first tab to the second tab via a global variable. The second tab then outputs the logging in the console. If you need to delve this deep into the roll20 client code anyway, did you consider to read it straight from the source?
1651675903

Edited 1651677197
Oosh
Sheet Author
API Scripter
Yep, reading straight from firebase is a much simpler method, but not one I'd really document here because Reasons. Also, those two lines of code don't do anything in and of themselves, unless you know what to replace campaign_storage_path with, and want to paste it into two tabs every time you refresh them. It's not quite a complete solution, though it is elegant :) But it would sidestep the horrible event echoes from the GM API... I don't have the patience to work out which particular variety of garbage is going on there, so it got slapped with a buffer.