Add Network agent

Summary: Adds methods in XMLHttpRequest so that the agent can hook the events needed for the implementation, these are only enabled if the agent is enabled (which means that the inspector is connected), it is also stripped out in non-dev currently.

Reviewed By: davidaurelio

Differential Revision: D4021516

fbshipit-source-id: c0c00d588404012d20b744de74e5ecbe5c002a53
This commit is contained in:
Alexander Blom 2016-11-02 12:18:17 -07:00 committed by Facebook Github Bot
parent 6a1783210b
commit 1709043a12
3 changed files with 367 additions and 2 deletions

View File

@ -177,7 +177,7 @@ navigator.product = 'ReactNative';
defineProperty(navigator, 'geolocation', () => require('Geolocation'));
// Set up collections
// We can't make these lazy because `Map` checks for `global.Map` (which would
// We can't make these lazy because `Map` checks for `global.Map` (which wouldc
// not exist if it were lazily defined).
defineProperty(global, 'Map', () => require('Map'), true);
defineProperty(global, 'Set', () => require('Set'), true);
@ -195,6 +195,12 @@ if (__DEV__) {
require('react-transform-hmr');
}
// Set up inspector
if (__DEV__) {
const JSInspector = require('JSInspector');
JSInspector.registerAgent(require('NetworkAgent'));
}
// Just to make sure the JS gets packaged up. Wait until the JS environment has
// been initialized before requiring them.
require('RCTDeviceEventEmitter');

View File

@ -0,0 +1,300 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NetworkAgent
* @flow
*/
'use strict';
const InspectorAgent = require('InspectorAgent');
const JSInspector = require('JSInspector');
const Map = require('Map');
const XMLHttpRequest = require('XMLHttpRequest');
import type EventSender from 'InspectorAgent';
type RequestId = string;
type LoaderId = string;
type FrameId = string;
type Timestamp = number;
type Headers = Object;
// We don't currently care about this
type ResourceTiming = null;
type ResourceType =
'Document' |
'Stylesheet' |
'Image' |
'Media' |
'Font' |
'Script' |
'TextTrack' |
'XHR' |
'Fetch' |
'EventSource' |
'WebSocket' |
'Manifest' |
'Other';
type SecurityState =
'unknown' |
'neutral' |
'insecure' |
'warning' |
'secure' |
'info';
type BlockedReason =
'csp' |
'mixed-content' |
'origin' |
'inspector' |
'subresource-filter' |
'other';
type StackTrace = null;
type Initiator = {
type: 'script' | 'other',
stackTrace?: StackTrace,
url?: string,
lineNumber?: number
}
type ResourcePriority = 'VeryLow' | 'Low' | 'Medium' | 'High' | 'VeryHigh';
type Request = {
url: string,
method: string,
headers: Headers,
postData?: string,
mixedContentType?: 'blockable' | 'optionally-blockable' | 'none',
initialPriority: ResourcePriority,
};
type Response = {
url: string,
status: number,
statusText: string,
headers: Headers,
headersText?: string,
mimeType: string,
requestHeaders?: Headers,
requestHeadersText?: string,
connectionReused: boolean,
connectionId: number,
fromDiskCache?: boolean,
encodedDataLength: number,
timing?: ResourceTiming,
securityState: SecurityState,
};
type RequestWillBeSentEvent = {
requestId: RequestId,
frameId: FrameId,
loaderId: LoaderId,
documentURL: string,
request: Request,
timestamp: Timestamp,
initiator: Initiator,
redirectResponse?: Response,
// This is supposed to be optional but the inspector crashes without it,
// see https://bugs.chromium.org/p/chromium/issues/detail?id=653138
type: ResourceType,
};
type ResponseReceivedEvent = {
requestId: RequestId,
frameId: FrameId,
loaderId: LoaderId,
timestamp: Timestamp,
type: ResourceType,
response: Response,
};
type DataReceived = {
requestId: RequestId,
timestamp: Timestamp,
dataLength: number,
encodedDataLength: number,
};
type LoadingFinishedEvent = {
requestId: RequestId,
timestamp: Timestamp,
encodedDataLength: number,
};
type LoadingFailedEvent = {
requestId: RequestId,
timestamp: Timestamp,
type: ResourceType,
errorText: string,
canceled?: boolean,
blockedReason?: BlockedReason,
};
class Interceptor {
_agent: NetworkAgent;
_requests: Map<string, string>;
constructor(agent: NetworkAgent) {
this._agent = agent;
this._requests = new Map();
}
getData(requestId: string): ?string {
return this._requests.get(requestId);
}
requestSent(
id: number,
url: string,
method: string,
headers: Object) {
const requestId = String(id);
this._requests.set(requestId, '');
const request: Request = {
url,
method,
headers,
initialPriority: 'Medium',
};
const event: RequestWillBeSentEvent = {
requestId,
documentURL: '',
frameId: '1',
loaderId: '1',
request,
timestamp: JSInspector.getTimestamp(),
initiator: {
// TODO(blom): Get stack trace
// If type is 'script' the inspector will try to execute
// `stack.callFrames[0]`
type: 'other',
},
type: 'Other',
};
this._agent.sendEvent('requestWillBeSent', event);
}
responseReceived(
id: number,
url: string,
status: number,
headers: Object) {
const requestId = String(id);
const response: Response = {
url,
status,
statusText: String(status),
headers,
// TODO(blom) refined headers, can we get this?
requestHeaders: {},
mimeType: this._getMimeType(headers),
connectionReused: false,
connectionId: -1,
encodedDataLength: 0,
securityState: 'unknown',
};
const event: ResponseReceivedEvent = {
requestId,
frameId: '1',
loaderId: '1',
timestamp: JSInspector.getTimestamp(),
type: 'Other',
response,
};
this._agent.sendEvent('responseReceived', event);
}
dataReceived(
id: number,
data: string) {
const requestId = String(id);
const existingData = this._requests.get(requestId) || '';
this._requests.set(requestId, existingData.concat(data));
const event: DataReceived = {
requestId,
timestamp: JSInspector.getTimestamp(),
dataLength: data.length,
encodedDataLength: data.length,
};
this._agent.sendEvent('dataReceived', event);
}
loadingFinished(
id: number,
encodedDataLength: number) {
const event: LoadingFinishedEvent = {
requestId: String(id),
timestamp: JSInspector.getTimestamp(),
encodedDataLength: encodedDataLength,
};
this._agent.sendEvent('loadingFinished', event);
}
loadingFailed(
id: number,
error: string) {
const event: LoadingFailedEvent = {
requestId: String(id),
timestamp: JSInspector.getTimestamp(),
type: 'Other',
errorText: error,
};
this._agent.sendEvent('loadingFailed', event);
}
_getMimeType(headers: Object): string {
const contentType = headers['Content-Type'] || '';
return contentType.split(';')[0];
}
}
type EnableArgs = {
maxResourceBufferSize?: number,
maxTotalBufferSize?: number
};
class NetworkAgent extends InspectorAgent {
static DOMAIN = 'Network';
_sendEvent: EventSender;
_interceptor: ?Interceptor;
enable({ maxResourceBufferSize, maxTotalBufferSize }: EnableArgs) {
this._interceptor = new Interceptor(this);
XMLHttpRequest.setInterceptor(this._interceptor);
}
disable() {
XMLHttpRequest.setInterceptor(null);
this._interceptor = null;
}
getResponseBody({requestId}: {requestId: RequestId})
: {body: ?string, base64Encoded: boolean} {
return {body: this.interceptor().getData(requestId), base64Encoded: false};
}
interceptor(): Interceptor {
if (this._interceptor) {
return this._interceptor;
} else {
throw Error('_interceptor can not be null');
}
}
}
module.exports = NetworkAgent;

View File

@ -11,9 +11,9 @@
*/
'use strict';
const EventTarget = require('event-target-shim');
const RCTNetworking = require('RCTNetworking');
const EventTarget = require('event-target-shim');
const base64 = require('base64-js');
const invariant = require('fbjs/lib/invariant');
const warning = require('fbjs/lib/warning');
@ -21,6 +21,28 @@ const warning = require('fbjs/lib/warning');
type ResponseType = '' | 'arraybuffer' | 'blob' | 'document' | 'json' | 'text';
type Response = ?Object | string;
type XHRInterceptor = {
requestSent: (
id: number,
url: string,
method: string,
headers: Object) => void,
responseReceived: (
id: number,
url: string,
status: number,
headers: Object) => void,
dataReceived: (
id: number,
data: string) => void,
loadingFinished: (
id: number,
encodedDataLength: number) => void,
loadingFailed: (
id: number,
error: string) => void,
};
const UNSENT = 0;
const OPENED = 1;
const HEADERS_RECEIVED = 2;
@ -68,6 +90,8 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
static LOADING: number = LOADING;
static DONE: number = DONE;
static _interceptor: ?XHRInterceptor = null;
UNSENT: number = UNSENT;
OPENED: number = OPENED;
HEADERS_RECEIVED: number = HEADERS_RECEIVED;
@ -109,6 +133,10 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
_trackingName: string = 'unknown';
_incrementalEvents: boolean = false;
static setInterceptor(interceptor: ?XHRInterceptor) {
XMLHttpRequest._interceptor = interceptor;
}
constructor() {
super();
this._reset();
@ -224,6 +252,12 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
// exposed for testing
__didCreateRequest(requestId: number): void {
this._requestId = requestId;
XMLHttpRequest._interceptor && XMLHttpRequest._interceptor.requestSent(
requestId,
this._url || '',
this._method || 'GET',
this._headers);
}
// exposed for testing
@ -257,6 +291,12 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
} else {
delete this.responseURL;
}
XMLHttpRequest._interceptor && XMLHttpRequest._interceptor.responseReceived(
requestId,
responseURL || this._url || '',
status,
responseHeaders || {});
}
}
@ -267,6 +307,10 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
this._response = response;
this._cachedResponse = undefined; // force lazy recomputation
this.setReadyState(this.LOADING);
XMLHttpRequest._interceptor && XMLHttpRequest._interceptor.dataReceived(
requestId,
response);
}
__didReceiveIncrementalData(
@ -283,6 +327,11 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
} else {
this._response += responseText;
}
XMLHttpRequest._interceptor && XMLHttpRequest._interceptor.dataReceived(
requestId,
responseText);
this.setReadyState(this.LOADING);
this.__didReceiveDataProgress(requestId, progress, total);
}
@ -322,6 +371,16 @@ class XMLHttpRequest extends EventTarget(...XHR_EVENTS) {
this._clearSubscriptions();
this._requestId = null;
this.setReadyState(this.DONE);
if (error) {
XMLHttpRequest._interceptor && XMLHttpRequest._interceptor.loadingFailed(
requestId,
error);
} else {
XMLHttpRequest._interceptor && XMLHttpRequest._interceptor.loadingFinished(
requestId,
this._response.length);
}
}
}