Skip to content

Instantly share code, notes, and snippets.

@notelho
Forked from mrharel/proxyTrack.js
Created December 18, 2019 14:37
Show Gist options
  • Select an option

  • Save notelho/b9c00e5efe31e8f52f52c1c50a18dad1 to your computer and use it in GitHub Desktop.

Select an option

Save notelho/b9c00e5efe31e8f52f52c1c50a18dad1 to your computer and use it in GitHub Desktop.

Revisions

  1. @mrharel mrharel created this gist Dec 28, 2018.
    239 changes: 239 additions & 0 deletions proxyTrack.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,239 @@
    const callerMap = {};

    function getCaller(error) {
    if (error && error.stack) {
    const lines = error.stack.split('\n');
    if (lines.length > 2) {
    let match = lines[2].match(/at ([a-zA-Z\-_$.]+) (.*)/);
    if (match) {
    return {
    name: match[1].replace(/^Proxy\./, ''),
    file: match[2],
    };
    } else {
    match = lines[2].match(/at (.*)/);
    if (match) {
    return {
    name: 'unknown',
    file: match[1],
    };
    }
    }
    }
    }
    return {
    name: 'unknown',
    file: '',
    };
    }

    function getFunctionName(fn, context) {
    let contextName = '';
    if (typeof context === 'function') {
    contextName = `{context.name}.`;
    } else if (context && context.constructor && context.constructor.name !== 'Object') {
    contextName = `${context.constructor.name}.`;
    }
    return `${contextName}${fn.name}`;
    }

    function trackFunctionCall(options = {}) {
    return function(target, thisArg, argumentsList) {
    const { trackTime, trackCaller, trackCount, stdout, filter } = options;
    const error = trackCaller && new Error();
    const caller = getCaller(error);
    const name = getFunctionName(target, thisArg);
    if (trackCount) {
    if (!callerMap[name]) {
    callerMap[name] = 1;
    } else {
    callerMap[name]++;
    }
    }
    let start, end;
    if (trackTime) {
    start = Date.now();
    }
    const retVal = target.apply(thisArg, argumentsList);
    if (trackTime) {
    end = Date.now();
    }
    let output = `${name} was called`;
    if (trackCaller) {
    output += ` by ${caller.name}`;
    }
    if (trackCount) {
    output += ` for the ${callerMap[name]} time`;
    }
    if (trackTime) {
    output += ` and took ${end-start} mils.`;
    }
    let canReport = true;
    if (filter) {
    canReport = filter({
    type: 'function',
    name,
    caller,
    count: callerMap[name],
    time: end - start,
    });
    }
    if (canReport) {
    if (stdout) {
    stdout(output);
    } else {
    console.log(output);
    }
    }
    return retVal;
    };
    }

    function trackPropertySet(options = {}) {
    return function set(target, prop, value, receiver) {
    const { trackCaller, trackCount, stdout, filter } = options;
    const error = trackCaller && new Error();
    const caller = getCaller(error);
    const contextName = target.constructor.name === 'Object' ? '' : `${target.constructor.name}.`;
    const name = `${contextName}${prop}`;
    const hashKey = `set_${name}`;
    if (trackCount) {
    if (!callerMap[hashKey]) {
    callerMap[hashKey] = 1;
    } else {
    callerMap[hashKey]++;
    }
    }
    let output = `${name} is being set`;
    if (trackCaller) {
    output += ` by ${caller.name}`;
    }
    if (trackCount) {
    output += ` for the ${callerMap[hashKey]} time`;
    }
    let canReport = true;
    if (filter) {
    canReport = filter({
    type: 'get',
    prop,
    name,
    caller,
    count: callerMap[hashKey],
    value,
    });
    }
    if (canReport) {
    if (stdout) {
    stdout(output);
    } else {
    console.log(output);
    }
    }
    return Reflect.set(target, prop, value, receiver);
    };
    }

    function trackPropertyGet(options = {}) {
    return function get(target, prop, receiver) {
    const { trackCaller, trackCount, stdout, filter } = options;
    if (typeof target[prop] === 'function' || prop === 'prototype') {
    return target[prop];
    }
    const error = trackCaller && new Error();
    const caller = getCaller(error);
    const contextName = target.constructor.name === 'Object' ? '' : `${target.constructor.name}.`;
    const name = `${contextName}${prop}`;
    const hashKey = `get_${name}`;

    if (trackCount) {
    if (!callerMap[hashKey]) {
    callerMap[hashKey] = 1;
    } else {
    callerMap[hashKey]++;
    }
    }
    let output = `${name} is being get`;
    if (trackCaller) {
    output += ` by ${caller.name}`;
    }
    if (trackCount) {
    output += ` for the ${callerMap[hashKey]} time`;
    }
    let canReport = true;
    if (filter) {
    canReport = filter({
    type: 'get',
    prop,
    name,
    caller,
    count: callerMap[hashKey],
    });
    }
    if (canReport) {
    if (stdout) {
    stdout(output);
    } else {
    console.log(output);
    }
    }
    return target[prop];
    };
    }

    function proxyFunctions(trackedEntity, options) {
    if (typeof trackedEntity === 'function') return;
    Object.getOwnPropertyNames(trackedEntity).forEach((name) => {
    if (typeof trackedEntity[name] === 'function') {
    trackedEntity[name] = new Proxy(trackedEntity[name], {
    apply: trackFunctionCall(options),
    });
    }
    });
    }

    function trackObject(obj, options = {}) {
    const { trackFunctions, trackProps } = options;

    let resultObj = obj;
    if (trackFunctions) {
    proxyFunctions(resultObj, options);
    }
    if (trackProps) {
    resultObj = new Proxy(resultObj, {
    get: trackPropertyGet(options),
    set: trackPropertySet(options),
    });
    }
    return resultObj;
    }

    const defaultOptions = {
    trackFunctions: true,
    trackProps: true,
    trackTime: true,
    trackCaller: true,
    trackCount: true,
    filter: null,
    };


    function trackClass(cls, options = {}) {
    cls.prototype = trackObject(cls.prototype, options);
    cls.prototype.constructor = cls;

    return new Proxy(cls, {
    construct(target, args) {
    const obj = new target(...args);
    return new Proxy(obj, {
    get: trackPropertyGet(options),
    set: trackPropertySet(options),
    });
    },
    apply: trackFunctionCall(options),
    });
    }

    export function proxyTrack(entity, options = defaultOptions) {
    if (typeof entity === 'function') return trackClass(entity, options);
    return trackObject(entity, options);
    }