NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Beautify Salesforce Debug View // @namespace SFDC // @version 0.2.9 // @description Beautify Salesforce Debug View // @author motiko // @license MIT // @match https://*.salesforce.com/p/setup/layout/ApexDebugLogDetailEdit/* // @require /src/libs/motiko/beautify.js // @require /src/libs/motiko/beautify-html.js // @resource debug_css https://raw.githubusercontent.com/motiko/sfdc-debug-logs/master/monkey/debug.css // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @run-at document-end // ==/UserScript== (function(){ if(typeof GM_getResourceText === "function"){ var debug_css = GM_getResourceText("debug_css"); GM_addStyle(debug_css); /* try{ var debug_css_local = GM_getResourceText("debug_css_local"); GM_addStyle(debug_css_local); }catch(e){} */ } var selectedText, currentResult, maxResult, keyPrefixes = [], sid = document.cookie.match(/(^|;\s*)sid=(.+?);/)[2], idRegex = /\b[a-zA-Z0-9]{18}\b|\b[a-zA-Z0-9]{15}\b/g, debugDescRegex = /(\d\d:\d\d:\d\d\.\d{3}\s+\(\d{8}\))\|(\w+)\|/, logEntryToDivTagClass = [{ logEntry: '|USER_DEBUG', divClass: 'debug' },{ logEntry: '|SYSTEM_', divClass: 'system systemMethodLog searchable' },{ logEntry: '|SOQL_EXECUTE_', divClass: 'soql searchable wrap' },{ logEntry: '|SOQL_EXECUTE_END', divClass: 'soql searchable wrap' },{ logEntry: '|METHOD_', divClass: 'method methodLog searchable' },{ logEntry: '|CONSTRUCTOR_', divClass: 'method methodLog searchable' },{ logEntry: '|EXCEPTION_', divClass: 'err searchable' },{ logEntry: '|FATAL_ERROR', divClass: 'err searchable' },{ logEntry: '|CODE_UNIT', divClass: 'method searchable' },{ logEntry: '|CALLOUT', divClass: 'callout searchable' },{ logEntry: '|VALIDATION_', divClass: 'method searchable' },{ logEntry: '|EXECUTION_', divClass: 'rest searchable' },{ logEntry: '|DML_BEGIN', divClass: 'rest searchable' },{ logEntry: '|DML_END', divClass: 'rest searchable' },{ logEntry: '|ENTERING_MANAGED_PKG', divClass: 'system systemMethodLog searchable' } ]; function setSetting(key,value){ if(typeof GM_setValue === "function"){ GM_setValue(key,value); }else{ localStorage.setItem(key,value); } } function getSetting(key){ if(typeof GM_getValue === "function"){ if(GM_getValue(key) === undefined) return false; return GM_getValue(key); }else{ return localStorage.getItem(key); } } init(); function init(){ document.body.addEventListener('keyup',keyUpListener); //document.body.addEventListener('mouseup',searchSelectedText); var codeElement = document.querySelector('pre'); var debugText = escapeHtml(codeElement.textContent); var res = debugText.split('\n').map(addTagsToKnownLines); res = res.reduce(toMultilineDivs); var codeBlock = document.querySelector('pre'); codeBlock.innerHTML = '<div class="monokai" id="debugText">' + res + '</div>'; document.querySelector('.oLeft').style.display ="none"; var oRight = document.querySelector('.oRight'); oRight.insertBefore(codeBlock,oRight.firstChild); addControllersContainer(); addDropDowns(); /*if(!getSetting('dontShowHint')){ addSearchHint(); }*/ removeIllegalIdLinks(); var debugElements = document.getElementsByClassName('debug'); var userDebugDivs = toArray(debugElements); userDebugDivs.forEach(function(debugDiv){ setTimeout(addExpnasionButtonsForUserDebugDivs.bind(null,debugDiv),0); }); addCollapseAllButton(); addCheckboxes(); } function addControllersContainer(){ var container = document.createElement('div'); container.id = 'controllersContainer'; addToTop(container); } function toArray(elemntList){ return [].slice.call(elemntList); } function addSearchHint(){ var hintContainer = document.createElement('div'); hintContainer.id = 'hintContainer'; var hint = document.createElement('span'); hint.innerHTML = ['<h4>Dear user,</h4><br/>', '<h7>Please note we changed keyboard shortcuts for your convenience.</h7> ', '<ul><li><b>Alt+Shift+d</b> - open debug logs </li>', '<li><b>Shift+d</b> - open debug logs in new tab </li></ul>', '<p> For full list of keyboard shortcuts please visit our <a href="https://github.com/motiko/sfdc-debug-logs">Github page</a></p>'].join(''); var hideTip = document.createElement('button'); hideTip.textContent = 'X'; hideTip.title = 'Close'; hideTip.className = 'closeButton'; hideTip.onclick = function(){ hintContainer.style.display = 'none'; setSetting('dontShowHint',true); }; hintContainer.appendChild(hideTip); hintContainer.appendChild(hint); addToTop(hintContainer); } function addDropDowns(){ addController(generateStyleSelect()); addController(generateFontSelect()); var savedStyle = getSetting('style'); if(savedStyle){ var styleSelection = document.querySelector('#styleSelection'); styleSelection.value = savedStyle; styleSelection.onchange(); } var savedFontSize = getSetting('fontSize'); var fontSize = savedFontSize || 18; var fontSizeSelection = document.querySelector('#fontSelection'); fontSizeSelection.value = fontSize; fontSizeSelection.onchange(); } function generateFontSelect(){ var selectStyleContainer = document.createElement('span'); selectStyleContainer.id = 'selectFontContainer'; var label = document.createElement('label'); label.textContent = 'Font Size:'; label.for = 'fontSelection'; var dropDown = document.createElement('select'); dropDown.id = 'fontSelection'; var size; for(size=12;size<29;size++){ var opt = document.createElement('option'); opt.value = size; opt.textContent = size; dropDown.appendChild(opt); } dropDown.onchange = function(event){ document.querySelector('#debugText').style.fontSize = this.value + 'px'; setSetting('fontSize',this.value); }; selectStyleContainer.appendChild(label); selectStyleContainer.appendChild(dropDown); return selectStyleContainer; } function generateStyleSelect(){ var selectStyleContainer = document.createElement('span'); selectStyleContainer.id = 'selectStyleContainer'; var label = document.createElement('label'); label.textContent = 'Pick Style: '; label.for = 'styleSelection'; var dropDown = document.createElement('select'); dropDown.id = 'styleSelection'; var styles = [{name:'monokai',label:'Monokai'},{name:'bw',label:'Black/White'},{name:'emacs',label:'Emacs'}]; styles.forEach(function(style){ var opt = document.createElement('option'); opt.value = style.name; opt.textContent = style.label; dropDown.appendChild(opt); }); dropDown.onchange = function(event){ document.querySelector('#debugText').className = this.value; setSetting('style',this.value); }; selectStyleContainer.appendChild(label); selectStyleContainer.appendChild(dropDown); return selectStyleContainer; } function addCheckboxes(){ var showSystemLabel = document.createElement('label'); showSystemLabel.className = 'toggleHidden'; showSystemLabel.innerHTML = '<input type="checkbox" name="checkbox" id="showSystem"/>Show <u>S</u>ystem Methods</label>'; var showMethodLogLabel = document.createElement('label'); showMethodLogLabel.className = 'toggleHidden'; showMethodLogLabel.innerHTML = '<input type="checkbox" name="checkbox" checked="checked" id="showUserMethod" />Show <u>U</u>ser Methods</label>'; var showTimeStamp = document.createElement('label'); showTimeStamp.className = 'toggleHidden'; showTimeStamp.innerHTML = '<input type="checkbox" name="checkbox" id="showTimestamps"/>Show <u>T</u>imestamps</label>'; addToTop(showMethodLogLabel); addToTop(showSystemLabel); addToTop(showTimeStamp); document.getElementById('showTimestamps').checked = JSON.parse(getSetting('showTimeStamp')); document.getElementById('showTimestamps').onchange = toggleTimestamp; document.getElementById('showUserMethod').onchange = toogleHidden('methodLog'); document.getElementById('showSystem').onchange = toogleHidden('systemMethodLog'); } function toggleTimestamp(){ var oldValue = JSON.parse(getSetting('showTimeStamp')); setSetting('showTimeStamp',!oldValue); document.location.reload(); } function addCollapseAllButton(){ var button = document.createElement('button'); button.innerHTML = '<u>E</u>xpand All'; button.onclick = colapseAll; button.id = 'collapseAllButton'; button.className = 'myButton'; addToTop(button); } function colapseAll(){ toArray(document.querySelectorAll('.expandUserDebugBtn.collapsed')).forEach(function(button){ setTimeout(expandUserDebug.bind(button),0); }); this.innerHTML = 'Collaps<u>e</u> All'; this.onclick = function(){ toArray(document.querySelectorAll('.expandUserDebugBtn.expanded')).forEach(function(button){ setTimeout(button.onclick.bind(button),0); }); this.innerHTML = '<u>E</u>xpand All'; this.onclick = colapseAll; }; } function addToTop(nodeElement){ document.querySelector('.codeBlock').insertBefore(nodeElement,document.getElementById('debugText')); } function addController(controller){ document.querySelector('#controllersContainer').appendChild(controller); } function keyUpListener(event){ /*if(event.keyCode == 70){ // 'f' if(currentResult === undefined || currentResult === maxResult){ currentResult = -1; } currentResult++; goToResult(currentResult); } else if(event.keyCode == 66){ // 'b' if(currentResult === undefined || currentResult === 0){ currentResult = maxResult + 1; } currentResult--; goToResult(currentResult); } else*/ if(event.keyCode == 27){ // 'esc' removeHighlightingOfSearchResults(); } else if(event.keyCode == 85){ // 'u' clickOn(document.querySelector('#showUserMethod')); } else if(event.keyCode == 83){ // 's' clickOn(document.querySelector('#showSystem')); } else if(event.keyCode == 69){ // 'e' clickOn(document.querySelector('#collapseAllButton')); } else if(event.keyCode == 84){ // 't' clickOn(document.querySelector('#showTimestamps')); } } function clickOn(nodeElement){ var event = document.createEvent('MouseEvents'); event.initEvent('click', true, true); nodeElement.dispatchEvent(event, true); } function goToResult(resultNum){ document.location.replace('#result' + resultNum); document.body.addEventListener('keyup',keyUpListener); var previouslySelectdElement = document.querySelector('.currentResult'); if(previouslySelectdElement){ previouslySelectdElement.classList.remove('currentResult'); } document.querySelector('span.highlightSearchResult[data-number="' + resultNum + '"]') .classList.add('currentResult'); } function removeHighlightingOfSearchResults(){ currentResult = 0; maxResult = 0; toArray(document.querySelectorAll('.highlightSearchResult') ).forEach(function(span){ var highlightedText = span.textContent; var textNode = document.createTextNode(highlightedText); span.parentElement.insertBefore(textNode,span); span.parentElement.removeChild(span); }); toArray(document.querySelectorAll('.searchResultAnchor') ).forEach(function(a){ a.parentElement.removeChild(a); }); } function searchSelectedText(event){ if(event.button == 2 || !event.altKey){ return; } selectedText = document.getSelection().toString(); removeHighlightingOfSearchResults(); if(!selectedText){ currentResult = 0; maxResult = 0; return; } selectedText = escapeHtml(selectedText); var searchableElements = toArray(document.querySelectorAll('#debugText .searchable') ); var resultNum =0 ; searchableElements.filter(notHidden).filter(contains(selectedText)).forEach(function(div){ var regExp = new RegExp(escapeRegExp(selectedText),'gi'); div.innerHTML = div.textContent.replace(regExp,function(match){ var resultString = '<a name="result' + resultNum + '" class="searchResultAnchor" data-number="' + resultNum + '"></a><span class="highlightSearchResult" data-number="' + resultNum + '">' + match +'</span>'; resultNum++; return resultString; }); div.innerHTML = div.innerHTML.replace(idRegex,withLegalIdLink); maxResult = resultNum-1; }); markNearestSearchResult(); } function notHidden(element){ return element.offsetParent !== null; } function markNearestSearchResult(){ var visibleSearchResults = toArray(document.querySelectorAll('.searchResultAnchor')) .filter(isVisibleElement); if(visibleSearchResults.length > 0){ var mouseY = event.clientY; var closest = visibleSearchResults.map(function(anchor){ var anchorY = anchor.getBoundingClientRect().top; return {element: anchor,distance:Math.abs(mouseY - anchorY)}; }).reduce(function(found,current){ if(current.distance < found.distance){ return current; } return found; }); currentResult = parseInt(closest.element.dataset.number,10); document.querySelector('span.highlightSearchResult[data-number="' + currentResult + '"]') .classList.add('currentResult'); } } function isVisibleElement(element){ return element.getBoundingClientRect().top >= 0; } function addExpnasionButtonsForUserDebugDivs(userDebugDiv){ var debugLevel = userDebugDiv.innerHTML.match(/\[\d+\](\|[A-Z]+\|)/); if(!debugLevel) return; debugLevel = debugLevel[1]; var debugParts = userDebugDiv.innerHTML.split(debugLevel); userDebugDiv.innerHTML = '<span class="debugHeader searchable">' + debugParts[0] + debugLevel +'</span> <span class="debugContent searchable">' + debugParts[1] + '</span>'; var debugText = unescapeHtml(debugParts[1]); if(looksLikeHtml(debugText) || looksLikeSfdcObject(debugText) || isJsonString(debugText)){ var buttonExpand = document.createElement('button'); buttonExpand.onclick = expandUserDebug; buttonExpand.onmouseup = haltEvent; buttonExpand.className = 'expandUserDebugBtn collapsed myButton'; buttonExpand.textContent = '+'; userDebugDiv.insertBefore(buttonExpand,userDebugDiv.children[0]); } } function toMultilineDivs(prevVal,curLine,index){ if(index == 1){ // handling first line return '<div class="rest">' + prevVal + '</div>' + curLine ; } else if(curLine.lastIndexOf('</div>') == curLine.length - '</div>'.length && curLine.length - '</div>'.length != -1){ // current line ends with <div> tag all good return prevVal + curLine; } else{ // expanding <div> to mutliline (e.g. LIMIT_USAGE_FOR_NS is multiline and cant recognise each line separately or USER_DEBUG with /n) return prevVal.substr(0,prevVal.length - '</div>'.length) + '\n' + curLine + '</div>'; } } function addTagsToKnownLines(curLine){ if(curLine.indexOf('Execute Anonymous:') === 0){ return '<div class="system searchable">' + curLine + '</div>'; } if(curLine.search(idRegex) > -1){ curLine = curLine.replace(idRegex,'<a href="/$&" class="idLink">$&</a>'); } var timeStampIndex = curLine.indexOf('|'); var cutLine; if(timeStampIndex > -1){ cutLine = curLine.substr(timeStampIndex + 1); } var resultTag = ''; var i; for(i=0; i<logEntryToDivTagClass.length ; i++){ if(curLine.indexOf(logEntryToDivTagClass[i].logEntry) > -1){ resultTag = '<div class="' + logEntryToDivTagClass[i].divClass + '">' + (JSON.parse(getSetting('showTimeStamp')) ? curLine : cutLine) + '</div>'; break; } } if(resultTag){ return resultTag; } var splitedDebugLine = curLine.split('|'); if(!splitedDebugLine || splitedDebugLine.length <= 2){ return curLine; } return '<div class="rest searchable">' + curLine +'</div>'; } function haltEvent(event){ event.stopPropagation(); } function removeIllegalIdLinks(){ request('/services/data/v29.0/sobjects').then(function(response){ var sobjects = JSON.parse(response).sobjects; keyPrefixes = sobjects.filter(function(sobj){ return (sobj.keyPrefix !== undefined); }).map(function(sobjectDescribe){ return sobjectDescribe.keyPrefix; }); keyPrefixes.push('03d'); // validation rule var idLinks = document.getElementsByClassName('idLink'); toArray(idLinks).forEach(function(link){ if(!isLegalId(link.textContent)){ link.className = 'disableClick'; } }); }); } function isLegalId(id){ return ( keyPrefixes.indexOf( id.substr(0,3) ) > -1 ); } function toogleHidden(className){ return function(event){ var systemLogs =toArray(document.getElementsByClassName(className)) ; systemLogs.forEach(function(systemLog){ systemLog.style.display = event.srcElement.checked ? 'block' : 'none'; }); }; } function expandUserDebug(){ var debugNode = this.nextElementSibling.nextElementSibling; var oldHtmlVal = debugNode.innerHTML; var debugNodeText = debugNode.textContent; if(looksLikeHtml(debugNodeText)){ debugNode.textContent = html_beautify(debugNodeText); }else if(looksLikeSfdcObject(debugNodeText)){ debugNode.textContent = js_beautify(sfdcObjectBeautify(debugNodeText)); }else if(isJsonString(debugNodeText)){ debugNode.textContent = js_beautify(debugNodeText); } if(debugNodeText.search(idRegex) > -1){ debugNode.innerHTML = debugNode.textContent.replace(idRegex,withLegalIdLink); } this.textContent = '-'; this.classList.add('expanded'); this.classList.remove('collapsed'); this.onclick = function(){ debugNode.innerHTML = oldHtmlVal; this.classList.remove('expanded'); this.classList.add('collapsed'); this.textContent = '+'; this.onclick = expandUserDebug; this.onmouseup = haltEvent; }; } function withLegalIdLink(id){ if(isLegalId(id)){ return '<a href="/' + id + '" class="idLink">' + id + '</a>'; } else{ return id; } } function sfdcObjectBeautify(string){ string = string.replace(/={/g,':{'); return string.replace(/([{| |\[]\w+)=(.+?)(?=, |},|}\)|:{|])/g,function(match,p1,p2){ return p1 + ":'" + p2 + "'" ; }); } function looksLikeSfdcObject(string){ return string.match(/\w+:{\w+=.+,?\s*}/); } function looksLikeHtml(source) { var trimmed = source.replace(/^[ \t\n\r]+/, ''); return (trimmed && trimmed.substring(0, 1) === '<'); } function isJsonString(str) { try { JSON.parse(str); } catch (e) { return false; } return true; } function escapeHtml(text) { var map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, function(m) { return map[m]; }); } function unescapeHtml(str) { return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '\''); } function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); } function contains(searchString){ return function(nodeElem){ return nodeElem.innerHTML.indexOf(searchString) > -1; }; } function request(url,method){ method = method || 'GET'; if(typeof GM_xmlhttpRequest === "function"){ return new Promise(function(fulfill,reject){ GM_xmlhttpRequest({ method:method, url:url, headers:{ Authorization:'Bearer ' + sid, Accept:'*/*' }, onload:function(response){ if( response.status.toString().indexOf('2') === 0){ fulfill(response.response); }else{ reject(Error(response.statusText)); } }, onerror:function(response){ rejected(Error("Network Error")); } }); }); } return new Promise(function(fulfill,reject){ var xhr = new XMLHttpRequest(); xhr.open(method,url); xhr.onload = function(){ if( xhr.status.toString().indexOf('2') === 0){ fulfill(xhr.response); }else{ reject(Error(xhr.statusText)); } }; xhr.onerror = function(){ rejected(Error("Network Error")); }; xhr.setRequestHeader('Authorization','Bearer ' + sid); xhr.send(); }); } })();