We override (monkey-patch) native Node insertion to the DOM, and wrap its content with performance marks. Later we send the duration of each execution along with the source domain of the script.
Last active
March 30, 2019 12:25
-
-
Save omrilotan/e31d97dea6600698ab9a8fe1e253512e to your computer and use it in GitHub Desktop.
Monitor performance of injected scripts (execution time)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
/** | |
* These functions will get "monkey-patched" | |
* @type {[String]} | |
*/ | |
const FUNCTIONS = ['appendChild', 'insertBefore']; | |
/** | |
* A random string to prevent accidental event override | |
* @type {String} | |
*/ | |
const PREFIX = Math.random().toString(36).substring(2); | |
/** | |
* Prevent collisions of multiple scripts from same domain by numbering them | |
* @type {Number} | |
*/ | |
let counter = 1; | |
/** | |
* Wrap text content of a DOM node | |
* @param {String} report | |
* @param {Node} element | |
* @return {undefined} | |
*/ | |
function wrap(report, element) { | |
try { | |
const { tagName, textContent } = element; | |
// Escape if the element is not a script tag or has no text content (e.g. <script src=) | |
if (!tagName || !textContent || tagName.toLowerCase() !== 'script') { return; } | |
/** | |
* Create a stack to retreive the origin of unattributed script | |
* @type {String} | |
*/ | |
const { stack = '' } = new Error(); | |
/** | |
* Extract source domain from the last line of the stack | |
* @type {String} | |
*/ | |
const [,source] = stack.split('\n').pop().match(/\/\/([\w\d\.]*)\//) || [,'unknown']; | |
/** | |
* A unique tag | |
* @type {String} | |
*/ | |
const tag = PREFIX + counter++; | |
/** | |
* Unique tags for "start" and "end" events | |
* @type {String} | |
*/ | |
const [start, end] = ['a', 'b'].map(n => [tag, n].join('_')); | |
// Wrap original content with measuring | |
element.textContent = [ | |
`performance.mark('${start}');`, | |
// Original text context (code) | |
textContent, | |
`performance.mark('${end}');`, | |
`performance.measure('${tag}', '${start}', '${end}');`, | |
// Send measurement to "report" function | |
'(function([{duration} = {}]) {', | |
` ${report}({source: '${source}', duration});`, | |
`})(performance.getEntriesByName('${tag}'));`, | |
// Cleanup | |
`performance.clearMarks('${start}');`, | |
`performance.clearMarks('${end}');`, | |
`performance.clearMeasures('${tag}');`, | |
].join('\n'); | |
} catch (e) { | |
// do nothing | |
} | |
} | |
/** | |
* Wrap injected content with a measuring mechanism | |
* @param {String} report Global method name (supports dot notation) | |
* @return {undefined} | |
*/ | |
function monitorInjection(report = 'console.log') { | |
if (!window.hasOwnProperty('performance')) { return; } | |
// Monkey-patch each of these functions | |
FUNCTIONS.forEach( | |
/** | |
* Monkey-patch the function with this name on Node prototype | |
* @param {String} fn | |
* @return {undefined} | |
*/ | |
fn => { | |
const original = Node.prototype[fn]; | |
/** | |
* Monkey-patched node functionality | |
* @param {...Any} args Original arguments | |
* @return {Any} Original response | |
*/ | |
Node.prototype[fn] = function(...args) { | |
wrap(report, ...args); | |
// Execute and return original functionality | |
return original.apply(this, args); | |
}; | |
} | |
); | |
} | |
// Register | |
monitorInjection(/* put alternative global report method here (string) */); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment