UNPKG

atom-nuclide

Version:

A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.

423 lines (348 loc) 12.7 kB
// This function will be injected into the target process. module.exports = function injection(require, debug, options) { var http = require('http'), https = require('https'), EventEmitter = require('events').EventEmitter; var addRequest = http.Agent.prototype.addRequest, onSocket = http.ClientRequest.prototype.onSocket; var lastRequestId = 0, lastConnectionId = 0; function timestamp() { return Date.now() / 1000000; } function Timing() { this._hrtime = process.hrtime(); this.json = { requestTime: Date.now() / 1000, proxyStart: -1, proxyEnd: -1, dnsStart: -1, dnsEnd: -1, connectStart: -1, connectEnd: -1, sslStart: -1, sslEnd: -1, serviceWorkerFetchStart: -1, serviceWorkerFetchReady: -1, serviceWorkerFetchEnd: -1, sendStart: -1, sendEnd: -1, receiveHeadersEnd: -1 }; } [ 'proxyStart', 'proxyEnd', 'dnsStart', 'dnsEnd', 'connectStart', 'connectEnd', 'sslStart', 'sslEnd', 'serviceWorkerFetchStart', 'serviceWorkerFetchReady', 'serviceWorkerFetchEnd', 'sendStart', 'sendEnd', 'receiveHeadersEnd' ].forEach(function(method) { Timing.prototype[method] = function() { var diff = process.hrtime(this._hrtime); this.json[method] = diff[0] * 1e3 + diff[1] / 1e6; return this; }; }); function getStackTrace() { var backup = Error.prepareStackTrace; Error.prepareStackTrace = function(_, stack) { return stack; }; // We want to trim stack trace to display user caller function. // We wait what request will be sent by one of functions in `callers` list. // If function exists in stack trace, his length will be great than 0 // If there is no known caller func we sent full stack trace var callers = [handleHttpRequest, http.request, http.get, https.request, https.get], error, stack; while (!stack && callers.length) { error = new Error(); Error.captureStackTrace(error, callers.pop()); if (error.stack.length) stack = error.stack; } if (!stack) stack = new Error().stack; Error.prepareStackTrace = backup; return stack.reduce(function(stack, frame) { var fileName = frame.getFileName(); fileName = debug.convert.v8NameToInspectorUrl(fileName); var url = fileName || frame.getEvalOrigin(); stack.push({ url: url, functionName: frame.getFunctionName(), lineNumber: frame.getLineNumber(), columnNumber: frame.getColumnNumber() }); return stack; }, []); } function renderHeaders(request) { var headers = request._headers; if (!headers) return {}; return Object.keys(headers).reduce(function renderHeader(result, key) { var headerName = request._headerNames[key]; result[headerName] = '' + headers[key]; return result; }, {}); } function getCorrectHeaders(response) { return Object.keys(response.headers).reduce(function correctHeader(result, key) { var value = response.headers[key]; if (Array.isArray(value)) value = value.join('\n'); result[key] = '' + value; return result; }, {}); } function getStatusText(response) { return response.statusMessage || http.STATUS_CODES[response.statusCode] || '?'; } function restoreHeadersText(response) { var headers = response.rawHeaders; // TODO: there is no `rawHeaders` in node 0.10.* // We can wrap parser to find raw headers, but it's not so useful to see original string // So, maybe won't fix if (!headers) return; var headersPrefix = [ 'HTTP/' + response.httpVersion, response.statusCode, response.statusMessage ].join(' '); var joinedHeaders = [headersPrefix]; while (headers.length) joinedHeaders.push(headers.splice(-2).join(': ')); return joinedHeaders.join('\r\n') + '\r\n\r\n'; } function getMimeType(response) { if (typeof response.headers['content-type'] != 'string') return 'text/plain'; return response.headers['content-type'].split(';')[0]; } function constructRequestInfo(request) { var requestId = request.__inspector_ID__; var timestamp = request.__inspector_timing__.json.requestTime; var url = request.__inspector_url__; return { requestId: requestId, loaderId: process.pid + '', documentURL: url, type: 'XHR', request: { headers: renderHeaders(request), method: request.method, postData: '', url: url }, timestamp: timestamp, initiator: { stackTrace: getStackTrace(), type: 'script' } }; } function constructResponseInfo(response) { var request = response.req; return { requestId: request.__inspector_ID__, loaderId: process.pid + '', timestamp: timestamp(), type: 'XHR', response: { url: request.__inspector_url__, status: response.statusCode, statusText: getStatusText(response), headers: getCorrectHeaders(response), mimeType: getMimeType(response), connectionReused: request.shouldKeepAlive, connectionId: request.connection.__inspector_ID__, encodedDataLength: -1, fromDiskCache: false, fromServiceWorker: false, timing: request.__inspector_timing__.json, headersText: restoreHeadersText(response), requestHeaders: renderHeaders(request), requestHeadersText: request._header } }; } function constructFailureInfo(request, err, canceled) { // NOTE: If there is no other `error` listeners // handling of `error` event changes program behavior // We won't to destroy app on each failed request, // but we need to notify user about unhandled `error` event. var unhandled = err && EventEmitter.listenerCount(request, 'error') === 0; var errorText = (unhandled ? '(unhandled) ' : '') + (err && err.code); return { requestId: request.__inspector_ID__, timestamp: timestamp(), type: 'XHR', errorText: errorText, canceled: canceled }; } function handleHttpRequest(options) { var timing = new Timing(); var protocol = options.protocol || this.agent.protocol || 'http:', host = options.host, port = options.port, path = this.path; this.__inspector_timing__ = timing; this.__inspector_ID__ = '' + lastRequestId++; this.__inspector_url__ = protocol + '//' + host + ':' + port + path; var requestInfo = constructRequestInfo(this); // NOTE: if request was called as `http.request(options, cb)` // then there is one action, which we can't detect in handleHttpResponse. // We need to send this information to handleHttpResponse. var willBeHandled = EventEmitter.listenerCount(this, 'response') > 0; this.once('socket', handleSocket.bind(this, requestInfo)); this.once('response', handleHttpResponse.bind(null, willBeHandled)); this.once('error', handleFailure.bind(this, requestInfo)); handleRequestData.call(this, requestInfo); handleAbort.call(this, requestInfo); timing.dnsStart(); } function handleRequestData(requestInfo) { var oldWrite = this.write; this.write = function(chunk) { requestInfo.request.postData += chunk || ''; return oldWrite.apply(this, arguments); }; } function sendRequestWillBeSent(requestInfo) { if (requestInfo.handled) return; requestInfo.handled = true; debug.emitEvent('Network.requestWillBeSent', requestInfo); debug.emitEvent('Network._requestWillBeSent', { requestId: requestInfo.requestId }); } function handleSocket(requestInfo, socket) { socket.__inspector_ID__ = socket.__inspector_ID__ || '' + lastConnectionId++; this.__inspector_timing__.dnsEnd().connectStart(); if (socket.writable) handleSocketConnect.call(this); else socket.once('connect', handleSocketConnect.bind(this)); sendRequestWillBeSent(requestInfo); } function handleSocketConnect() { this.__inspector_timing__.connectEnd().sendStart(); setImmediate(this.__inspector_timing__.sendEnd.bind(this.__inspector_timing__)); } function handleAbort(requestInfo) { var abort = this.abort; this.abort = function() { var result = abort.apply(this, arguments); handleFailure.call(this, requestInfo); return result; }; } function handleFailure(requestInfo, err) { sendRequestWillBeSent(requestInfo); var failureInfo = constructFailureInfo(this, err, !err); debug.emitEvent('Network.loadingFailed', failureInfo); debug.emitEvent('Network._loadingFailed', { requestId: this.__inspector_ID__ }); } function handleHttpResponse(wasHandled, response) { var request = response.req; request.__inspector_timing__.receiveHeadersEnd(); // NOTE: If there is no other `response` listeners // handling of `response` event changes program behavior // Without our listener all data will be dumped, but we pause data by our listener. // Most simple solution here to `resume` data stream, instead of dump it, // otherwise we'll never get a data. if (!wasHandled && EventEmitter.listenerCount(request, 'response') === 0) response.resume(); var requestId = request.__inspector_ID__, responseInfo = constructResponseInfo(response), dataLength = 0; debug.emitEvent('Network.responseReceived', responseInfo); var push = response.push; response.push = function(chunk) { if (chunk) { dataLength += chunk.length; debug.emitEvent('Network._dataReceived', { requestId: requestId, data: chunk + '' }); debug.emitEvent('Network.dataReceived', { requestId: requestId, timestamp: timestamp(), dataLength: chunk.length, encodedDataLength: chunk.length }); } return push.apply(this, arguments); }; response.once('end', function() { response.push = push; debug.emitEvent('Network._loadingFinished', { requestId: requestId }); debug.emitEvent('Network.loadingFinished', { requestId: requestId, timestamp: timestamp(), encodedDataLength: dataLength }); }); } debug.registerEvent('Network.requestWillBeSent'); debug.registerEvent('Network._requestWillBeSent'); debug.registerEvent('Network.responseReceived'); debug.registerEvent('Network.dataReceived'); debug.registerEvent('Network._dataReceived'); debug.registerEvent('Network.loadingFinished'); debug.registerEvent('Network._loadingFinished'); debug.registerEvent('Network.loadingFailed'); debug.registerEvent('Network._loadingFailed'); wrapHttpRequests(); debug.once('close', unwrapHttpRequests); function wrapHttpRequests() { // We can't wrap only `onSocket` for handling all requests, // there is one case, when `onSocket` will be never called // (Agent + no free sockets) http.Agent.prototype.addRequest = function(req, options) { // NOTE: For node 0.10.* // We can't redefine options itself, this changes arguments var _options = options; // Legacy API: addRequest(req, host, port, path) if (typeof _options === 'string') { _options = { host: options, port: arguments[2], path: arguments[3] }; } handleHttpRequest.call(req, _options); return addRequest.apply(this, arguments); }; http.ClientRequest.prototype.onSocket = function(socket) { var handledByAddRequest = this.__inspector_ID__ !== undefined, isUnixSocket = this.socketPath, options; if (!handledByAddRequest && !isUnixSocket) { options = debug.getFromFrame(1, 'options'); if (!options || !(options instanceof Object)) { console.error( 'Unexpected state in node-inspector network profiling.\n' + 'Something wrong with request `options` object in frame #1\n' + 'Current stackstace:', new Error().stack ); } else { handleHttpRequest.call(this, { host: options.host, port: options.port, path: this.path }); } options = null; } return onSocket.apply(this, arguments); }; } function unwrapHttpRequests() { http.Agent.prototype.addRequest = addRequest; http.ClientRequest.prototype.onSocket = onSocket; } };