diff --git a/Libraries/Core/InitializeCore.js b/Libraries/Core/InitializeCore.js index 8e5ae36f3..361a2a06f 100644 --- a/Libraries/Core/InitializeCore.js +++ b/Libraries/Core/InitializeCore.js @@ -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'); diff --git a/Libraries/JSInspector/NetworkAgent.js b/Libraries/JSInspector/NetworkAgent.js new file mode 100644 index 000000000..4314411b8 --- /dev/null +++ b/Libraries/JSInspector/NetworkAgent.js @@ -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; + + 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; diff --git a/Libraries/Network/XMLHttpRequest.js b/Libraries/Network/XMLHttpRequest.js index edcaf4b5d..a57ac3a9c 100644 --- a/Libraries/Network/XMLHttpRequest.js +++ b/Libraries/Network/XMLHttpRequest.js @@ -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); + } } }