sandworm-hq/sandworm-js

View on GitHub
src/track.js

Summary

Maintainability
B
4 hrs
Test Coverage
C
78%
import logger from './logger';
import platform, {PLATFORMS} from './platform';

let http;
try {
  http = __non_webpack_require__('http');
  // eslint-disable-next-line no-empty
} catch (error) {}
const hasXMLHTTPRequest = typeof XMLHttpRequest !== 'undefined';

if (!http && !hasXMLHTTPRequest) {
  logger.error('tracking disabled for this platform: no HTTP or XMLHttpRequest available');
}

const originals = {};
let batch = [];
let host = '127.0.0.1';
let port = 7071;
let currentTimer;
let skipTracking = false;

export const setSkipTracking = (skipTrackingOption) => {
  skipTracking = !!skipTrackingOption;
};

// Grab the original methods before we monkey patch them
// so that tracking calls do not get tracked causing an infinite loop
if (http) {
  originals.http = {
    request: http.request,
  };
}
if (hasXMLHTTPRequest) {
  originals.xmlhttprequest = {
    XMLHttpRequest,
    open: XMLHttpRequest.prototype.open,
    send: XMLHttpRequest.prototype.send,
    setRequestHeader: XMLHttpRequest.prototype.setRequestHeader,
  };
}

// Remove circular references from method invoke arguments getting
// converted to JSON to be tracked
export const getCircularReplacer = () => {
  const seen = new WeakSet();
  return (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) {
        return undefined;
      }
      seen.add(value);
    }
    return value;
  };
};

export const sendBatch = () => {
  try {
    if (skipTracking || batch.length === 0) {
      return;
    }
    const currentBatch = [...batch];
    const resetBatch = () => {
      batch = [...currentBatch, ...batch];
    };
    logger.debug('sending tracking...');
    if (platform() === PLATFORMS.NODE && http) {
      const req = originals.http.request(
        {
          port,
          host,
          path: '/ingest',
          method: 'POST',
          headers: {'content-type': 'application/json'},
        },
        (res) => {
          if (res.statusCode !== 200) {
            logger.debug('error tracking call to inspector: got status', res.statusCode);
            resetBatch();
          }
        },
      );
      req.on('error', (error) => {
        logger.debug('error tracking call to inspector:', error.message);
        resetBatch();
      });
      req.end(JSON.stringify(currentBatch, getCircularReplacer()));
      batch = [];
    } else if (hasXMLHTTPRequest) {
      const request = new originals.xmlhttprequest.XMLHttpRequest();
      request.onreadystatechange = () => {
        if (request.readyState === 4) {
          if (request.status !== 200) {
            logger.debug('error tracking call to inspector:', request.statusText);
            resetBatch();
          }
        }
      };
      request.addEventListener('error', () => {
        logger.debug('error tracking call to inspector');
        resetBatch();
      });
      originals.xmlhttprequest.open.call(request, 'POST', `http://${host}:${port}/ingest`, true);
      originals.xmlhttprequest.setRequestHeader.call(
        request,
        'content-type',
        'application/json;charset=UTF-8',
      );
      originals.xmlhttprequest.send.call(
        request,
        JSON.stringify(currentBatch, getCircularReplacer()),
      );
      batch = [];
    }
  } catch (error) {
    logger.debug('error tracking call to inspector:', error.message);
    logger.debug('attempted to track:', batch);
  } finally {
    currentTimer = null;
  }
};

export const setTrackingServer = (hostOption, portOption) => {
  if (hostOption && typeof hostOption === 'string') {
    host = hostOption;
  }
  if (portOption && typeof portOption === 'number') {
    port = portOption;
  }
};

export default (event) => {
  if (!event || typeof event !== 'object') {
    logger.warn('track: event is not an object');
    return;
  }

  // Add to queue and debounce sending to server
  batch.push({...event});

  if (!currentTimer) {
    currentTimer = setTimeout(sendBatch, 1000);
  }
};