Jimmy Breck-McKye

A lazy programmer

Spying on Undocumented Methods in JavaScript APIs

Occassionally you need to do this. You might want to capture performance or debug data to log elsewhere. We wanted to get the timing data from inside DFP and pass it to Ophan, our internal analytics solution.

Obviously, it has to be functionality you can live with failing.

  • Assume the API can change. All paths on the object can break. Work defensively.
  • Remember to call the underlying function and return its value. Leave no trace – even if blocking the original function doesn’t break anything today, it might tomorrow.
  • If the action is performance-critical, consider just logging the data somewhere on the heap and handling it later (I’ve written a run-when-cold fn for this)
  • Write a unit test that verifies the code won’t break when the undocumented feature is withdrawn

function spyOnMethodIfExists(object, path, interceptor) {

var pathParts = path.split('.');

}

gpt, ‘debug_log.log’ gpt, [‘debug_log’, ‘log’]

function fieldExists(object, fieldPath) {

if (typeof fieldPath === 'string') {
    fieldPath = path.split('.');
}

var stepToFirstField = fieldPath.shift();
var firstField = object[stepToFirstField];
var atFinalLocation = fieldPath.length === 0;

if (atFinalLocation) {
    return (firstField !== undefined);
} else if (thisField === null || thisField === undefined) {
    return false;
} else {
    return fieldExists(firstField, fieldPath);
}

}

function queryObject(object, path) {

if (typeof path === 'string') {
    path = path.split('.');
}

var stepToFirstField = path.shift();
var firstField = object[stepToFirstField];
var atFinalLocation = path.length === 0;

if (atFinalLocation) {
    return firstField;
} else if (firstField === null || firstField === undefined) {
    return undefined;
} else {
    return queryObject(firstField, path);
}

}

then

const logOnceCold = deferUntilCold(logGptPerfData);

if (fieldExists(googletag, ‘debug_log.log’)) {

const originalLogger = googletag.debug_log.log;
googletag.debug_log.log = function interceptedLogger() {
    const args = Array.prototype.slice.call(arguments, 0);
    logOnceCold(args);
    return originalLogger.apply(window, args);
}

}

function DeferredProcessor(action, timeout) {

var processScheduled = false;
var actionQueue = [];

this.queueProcess = function (data) {
    actionQueue.push(data);
    if (processScheduled === false) {
        scheduleProcess();
    }
};

function scheduleProcess() {
    window.setTimeout(executeQueue, timeout);
    processScheduled = true;
}

function executeQueue() {
    while (actionQueue.length) {
        action(actionQueue.shift());
    }
}

}

function deferUntilCold(action, cooldownTime) {

var actionCallQueue = [];
var scheduledProcess = null;

return function(data) {
    actionCallQueue.push(data);
    registerHeat();
};

function registerHeat() {
    if (scheduledProcess) {
        window.clearTimeout(scheduledProcess);
    }
    scheduledProcess = window.setTimeout(processQueue, cooldownTime);
}

function processQueue() {
    while (actionCallQueue.length) {
        action(actionCallQueue.shift());
    }
}

}

// Example

intercept(googletag, ‘debug_log.log’, logGptDebugData);

function intercept(object, path, interceptor) {

var pathParts = path.split('.');

var targetParentPath = pathParts.slice(-1);
var targetName = pathParts[pathParts.length - 1];

var targetParent = queryObject(object, targetParentPath);
var target = targetParent ? targetParent[targetName] : null;

if (target && typeof target === 'function') {
    var originalMethod = targetParent[targetName];
    targetParent[targetName] = function intercepted() {
        var args = Array.prototype.slice.call(arguments, 0);
        interceptor(args);
        return originalMethod.apply(targetParent, args);
    };
}

}

function interceptMethod(parent, name, interceptor) {

var originalMethod = parent[name];

if (typeof originalMethod === 'function') {
    parent[name] = function interceptor() {
        var args = Array.prototype.slice.call(arguments, 0);
        try {
            interceptor.apply(parent, args);
        } catch (e) {
            window.setTimeout(function () {throw e;}, 0);
        }
        return originalMethod.apply(parent, args);
    };
}

}

var targetExists = queryObject(googletag, ‘debug_log.log’); var logGptPerfDataWhenIdle = deferUntilCold(logGptPerfData, 1000);

if (targetExists) {

interceptMethod(googletag.debug_log, 'log', logGptPerfDataWhenIdle);

} else {

logWarning('Could not intercept GPT')

}

Comments