diff --git a/README.md b/README.md index 81337e881..5709f89c9 100644 --- a/README.md +++ b/README.md @@ -139,9 +139,9 @@ pod install react-native link instabug-reactnative ``` -### Upgrading from 8.0.3 to 8.x +### Upgrading in version 8 -When upgrading from 8.0.3 to 8.x, please make sure you do the following steps: +When doing an upgrade in these two cases, from 8.0.3 to 8.x or from an older version than 8.2.6 to 8.2.6 or higher, please make sure you do the following steps: 1. Unlink the project before upgrading to the new version ```bash @@ -197,6 +197,18 @@ export INSTABUG_APP_TOKEN="YOUR_APP_TOKEN" bash "../node_modules/instabug-reactnative/ios/upload_sourcemap.sh" ``` +## Network Logging + +Instabug network logging is enabled by default. It intercepts any requests performed with `fetch` or `XMLHttpRequest` and attaches them to the report that will be sent to the dashboard. To disable network logs: + +```javascript +import { NetworkLogger } from 'instabug-reactnative'; +``` + +```javascript +NetworkLogger.setEnabled(false); +``` + ## Documentation For more details about the supported APIs and how to use them, check our [**Documentation**](https://docs.instabug.com/docs/react-native-overview). diff --git a/android/build.gradle b/android/build.gradle index 33ced30b7..15d56eb22 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,7 +25,7 @@ android { dependencies { implementation 'com.facebook.react:react-native:+' - api ('com.instabug.library:instabug:8.2.2'){ + api ('com.instabug.library:instabug:8.2.2.0'){ exclude group: 'com.android.support:appcompat-v7' } } diff --git a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java index b7c8f7efa..c6ab1c9a9 100644 --- a/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java +++ b/android/src/main/java/com/instabug/reactlibrary/RNInstabugReactnativeModule.java @@ -12,6 +12,7 @@ import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableMap; @@ -73,6 +74,8 @@ import java.util.Locale; import java.util.Map; +import static com.instabug.reactlibrary.utils.InstabugUtil.getMethod; + /** * The type Rn instabug reactnative module. @@ -490,7 +493,7 @@ public void setCrashReportingEnabled(boolean isEnabled) { private void sendJSCrashByReflection(String exceptionObject, boolean isHandled) { try { JSONObject newJSONObject = new JSONObject(exceptionObject); - Method method = InstabugUtil.getMethod(Class.forName("com.instabug.crash.CrashReporting"), "reportException", JSONObject.class, boolean.class); + Method method = getMethod(Class.forName("com.instabug.crash.CrashReporting"), "reportException", JSONObject.class, boolean.class); if (method != null) { method.invoke(null, newJSONObject, isHandled); } @@ -1885,6 +1888,53 @@ public void setEmailFieldRequiredForFeatureRequests(boolean isEmailRequired, Rea } } + /** + * Extracts HTTP connection properties. Request method, Headers, Date, Url and Response code + * + * @param jsonObject the JSON object containing all HTTP connection properties + * @throws JSONException + */ + @ReactMethod + public void networkLog(String jsonObject) throws JSONException { + NetworkLog networkLog = new NetworkLog(); + String date = System.currentTimeMillis()+""; + networkLog.setDate(date); + JSONObject newJSONObject = new JSONObject(jsonObject); + networkLog.setUrl(newJSONObject.getString("url")); + networkLog.setRequest(newJSONObject.getString("requestBody")); + networkLog.setResponse(newJSONObject.getString("responseBody")); + networkLog.setMethod(newJSONObject.getString("method")); + networkLog.setResponseCode(newJSONObject.getInt("responseCode")); + networkLog.setRequestHeaders(newJSONObject.getString("requestHeaders")); + networkLog.setResponseHeaders(newJSONObject.getString("responseHeaders")); + networkLog.insert(); + } + + + @ReactMethod + public void setSecureViews(ReadableArray ids) { + int[] arrayOfIds = new int[ids.size()]; + for (int i = 0; i < ids.size(); i++) { + int viewId = (int) ids.getDouble(i); + arrayOfIds[i] = viewId; + } + Method method = null; + try { + method = InstabugUtil.getMethod(Class.forName("com.instabug.library.Instabug"), "setSecureViewsId", int[].class); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + if (method != null) { + try { + method.invoke(null, arrayOfIds); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + } + private InstabugCustomTextPlaceHolder.Key getStringToKeyConstant(String key) { switch (key) { case SHAKE_HINT: diff --git a/index.d.ts b/index.d.ts index ba7fed274..3dc687d06 100644 --- a/index.d.ts +++ b/index.d.ts @@ -112,6 +112,12 @@ export namespace Surveys { ): void; function setShouldShowWelcomeScreen(shouldShowWelcomeScreen: boolean): void; } +export namespace NetworkLogger { + function setEnabled(isEnabled: boolean): void; + function setNetworkDataObfuscationHandler(handler: () => void): void; + function setRequestFilterExpression(expression: string): void; + function setProgressHandlerForRequest(handler: () => void): void; +} export function startWithToken( token: string, invocationEvent: invocationEvent[] diff --git a/index.js b/index.js index 93844b515..1cc88a1a7 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ import { NativeAppEventEmitter, DeviceEventEmitter, Platform, + findNodeHandle, processColor } from 'react-native'; let { Instabug } = NativeModules; @@ -13,8 +14,10 @@ import FeatureRequests from './modules/FeatureRequests'; import Chats from './modules/Chats'; import Replies from './modules/Replies'; import CrashReporting from './modules/CrashReporting'; +import NetworkLogger from './modules/NetworkLogger'; captureJsErrors(); +NetworkLogger.setEnabled(true); /** * Instabug @@ -220,7 +223,7 @@ const InstabugModule = { * @param {color} primaryColor A color to set the UI elements of the SDK to. */ setPrimaryColor: function(primaryColor) { - Instabug.setPrimaryColor(primaryColor); + Instabug.setPrimaryColor(processColor(primaryColor)); }, /** @@ -692,6 +695,18 @@ const InstabugModule = { } }, + /** + * Hides component from screenshots, screen recordings and view hierarchy. + * @param {Object} viewRef the ref of the component to hide + */ + setPrivateView: function(viewRef) { + const nativeTag = findNodeHandle(viewRef); + if (Platform.OS === 'ios') { + Instabug.hideView(nativeTag); + } else { + Instabug.setSecureViews([nativeTag]); + } + }, /** * Shows default Instabug prompt. */ @@ -960,7 +975,8 @@ export { FeatureRequests, Chats, Replies, - CrashReporting -} + CrashReporting, + NetworkLogger +}; export default InstabugModule; diff --git a/ios/RNInstabug/InstabugReactBridge.m b/ios/RNInstabug/InstabugReactBridge.m index f4ab62f8f..11e57b92c 100644 --- a/ios/RNInstabug/InstabugReactBridge.m +++ b/ios/RNInstabug/InstabugReactBridge.m @@ -14,6 +14,7 @@ #import #import #import +#import @implementation InstabugReactBridge @@ -26,6 +27,7 @@ @implementation InstabugReactBridge @"IBGWillShowSurvey", @"IBGDidDismissSurvey", @"IBGDidSelectPromptOptionHandler", + @"IBGSetNetworkDataObfuscationHandler", @"IBGOnNewReplyReceivedCallback" ]; } @@ -45,13 +47,15 @@ - (dispatch_queue_t)methodQueue { [Instabug startWithToken:token invocationEvents:invocationEvents]; RCTAddLogFunction(InstabugReactLogFunction); RCTSetLogThreshold(RCTLogLevelInfo); - IBGNetworkLogger.enabled = NO; + SEL setCrossPlatformSEL = NSSelectorFromString(@"setCrossPlatform:"); if ([[Instabug class] respondsToSelector:setCrossPlatformSEL]) { [[Instabug class] performSelector:setCrossPlatformSEL withObject:@(true)]; } + IBGNetworkLogger.enabled = YES; [self setBaseUrlForDeprecationLogs]; + } RCT_EXPORT_METHOD(callPrivateApi:(NSString *)apiName apiParam: (NSString *) param) { @@ -529,6 +533,44 @@ - (dispatch_queue_t)methodQueue { callback(@[[NSNumber numberWithBool:result]]); } +RCT_EXPORT_METHOD(networkLog:(NSDictionary *) networkData) { + NSString* url = networkData[@"url"]; + NSString* method = networkData[@"method"]; + NSString* requestBody = networkData[@"requestBody"]; + NSString* responseBody = networkData[@"responseBody"]; + int32_t responseCode = [networkData[@"responseCode"] integerValue]; + NSDictionary* requestHeaders = networkData[@"requestHeaders"]; + NSDictionary* responseHeaders = networkData[@"responseHeaders"]; + NSString* contentType = networkData[@"contentType"]; + double duration = [networkData[@"duration"] doubleValue]; + + SEL networkLogSEL = NSSelectorFromString(@"addNetworkLogWithUrl:method:requestBody:responseBody:responseCode:requestHeaders:responseHeaders:contentType:duration:"); + + if([[IBGNetworkLogger class] respondsToSelector:networkLogSEL]) { + NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[[IBGNetworkLogger class] methodSignatureForSelector:networkLogSEL]]; + [inv setSelector:networkLogSEL]; + [inv setTarget:[IBGNetworkLogger class]]; + + [inv setArgument:&(url) atIndex:2]; + [inv setArgument:&(method) atIndex:3]; + [inv setArgument:&(requestBody) atIndex:4]; + [inv setArgument:&(responseBody) atIndex:5]; + [inv setArgument:&(responseCode) atIndex:6]; + [inv setArgument:&(requestHeaders) atIndex:7]; + [inv setArgument:&(responseHeaders) atIndex:8]; + [inv setArgument:&(contentType) atIndex:9]; + [inv setArgument:&(duration) atIndex:10]; + + [inv invoke]; + } +} + +RCT_EXPORT_METHOD(hideView: (nonnull NSNumber *)reactTag) { + UIView* view = [self.bridge.uiManager viewForReactTag:reactTag]; + view.instabug_privateView = true; + +} + RCT_EXPORT_METHOD(show) { [Instabug show]; } @@ -583,6 +625,7 @@ - (dispatch_queue_t)methodQueue { } else { IBGReplies.didReceiveReplyHandler = nil; } + } - (NSDictionary *)constantsToExport diff --git a/link_bridge.js b/link_bridge.js index 4414fa8c5..e6d968757 100755 --- a/link_bridge.js +++ b/link_bridge.js @@ -1,2 +1,3 @@ const exec = require('child_process').exec; +require('./link_gradle'); exec("ruby ./node_modules/instabug-reactnative/link.rb || echo \"Ruby doesn't exist, if you're building this for Android only, then feel free to ignore this error, otherwise please install Ruby and run 'react-native link instabug-reactnative' again\""); diff --git a/link_gradle.js b/link_gradle.js new file mode 100644 index 000000000..e503aa283 --- /dev/null +++ b/link_gradle.js @@ -0,0 +1,116 @@ +'use strict'; +var fs = require('fs'); + +const LOG_LEVEL_SUCCESS = 0; +const LOG_LEVEL_WARN = 1; + +const CHAR_OPEN_PARAN = '{'; +const CHAR_CLOSED_PARAN = '}'; + +const GRADLE_FILE_PATH = 'android/build.gradle'; +const MAVEN_REPO_URL = + 'https://sdks.instabug.com/nexus/repository/instabug-cp'; + +function getPosition(string, substring, occurrenceInString) { + return string.split(substring, occurrenceInString).join(substring).length; +} + +function findRepositoriesBlockEnd(block) { + let repositoriesStartBlockIndex = getPosition(block, CHAR_OPEN_PARAN, 2); + let count = 1; + let blockEndIndex = -1; + for (let i = repositoriesStartBlockIndex + 1; i < block.length; i++) { + if (block.charAt(i) === CHAR_OPEN_PARAN) { + count++; + } + + if (block.charAt(i) === CHAR_CLOSED_PARAN) { + count--; + } + + if (count === 0) { + blockEndIndex = i; + break; + } + } + + return blockEndIndex; +} + +function readFile(filePath, success) { + fs.readFile(filePath, 'utf-8', function(err, data) { + if (err) { + console.log(process.cwd()); + finish( + LOG_LEVEL_WARN, + `Linking process could not be completed because of\n${err.message}` + ); + } + success(data); + }); +} + +function writeFile(data) { + fs.writeFile(GRADLE_FILE_PATH, data, err => { + if (err) { + finish( + LOG_LEVEL_WARN, + `Linking process could not be completed because of\n${err.message}` + ); + } + finish(LOG_LEVEL_SUCCESS, 'Linking process completed successfully'); + }); +} + +function finish(logLevel, message) { + if (logLevel === LOG_LEVEL_SUCCESS) { + console.info(message); + } else { + console.warn(message); + } + + process.exit(0); +} + +function generateNewGradleFile(data) { + + if (data.includes(MAVEN_REPO_URL)) { + finish(LOG_LEVEL_SUCCESS, ''); + } + + const regex = /allprojects\ *\n*\ *{/; + if (!regex.test(data)) { + finish( + LOG_LEVEL_WARN, + 'Something went wrong while trying to complete the linking process. ' + ); + } + + + const matchedRegex = data.match(regex); + const block = data.substring(matchedRegex.index, data.length); + const blockEndIndex = findRepositoriesBlockEnd(block); + + if (blockEndIndex === -1) { + finish( + LOG_LEVEL_WARN, + 'Something went wrong while trying to complete the linking process. ' + ); + } + + let updatedBlock = `${block.substring(0, blockEndIndex)}\tmaven { + \t url "${MAVEN_REPO_URL}" + \t} + ${block.substring(blockEndIndex, block.length)}`; + const newGradleFile = `${data.substring( + 0, + matchedRegex.index + )}${updatedBlock}`; + return newGradleFile; +} + + +readFile(GRADLE_FILE_PATH, function(data) { + const newFile = generateNewGradleFile(data) + writeFile(newFile); +}); diff --git a/modules/NetworkLogger.js b/modules/NetworkLogger.js new file mode 100644 index 000000000..df4bd87a8 --- /dev/null +++ b/modules/NetworkLogger.js @@ -0,0 +1,88 @@ +import { NativeModules, Platform } from 'react-native'; +import xhr from '../utils/XhrNetworkInterceptor'; +import IBGEventEmitter from '../utils/IBGEventEmitter.js'; +import InstabugConstants from '../utils/InstabugConstants'; +let { Instabug } = NativeModules; + + +var _networkDataObfuscationHandlerSet = false; +var _requestFilterExpression = false; + +/** + * NetworkLogger + * @exports NetworkLogger + */ +export default { + /** + * Sets whether network logs should be sent with bug reports. + * It is enabled by default. + * @param {boolean} isEnabled + */ + setEnabled(isEnabled) { + if (isEnabled) { + xhr.enableInterception(); + xhr.setOnDoneCallback(network => { + if (!eval(_requestFilterExpression)) { + if (_networkDataObfuscationHandlerSet) { + IBGEventEmitter.emit( + InstabugConstants.NETWORK_DATA_OBFUSCATION_HANDLER_EVENT, + network + ); + } else { + if (Platform.OS === 'android') { + Instabug.networkLog(JSON.stringify(network)); + } else { + Instabug.networkLog(network); + } + } + } + }); + } else { + xhr.disableInterception(); + } + }, + + /** + * Obfuscates any response data. + * @param {function} handler + */ + setNetworkDataObfuscationHandler(handler) { + if (handler !== null) { + _networkDataObfuscationHandlerSet = true; + } + IBGEventEmitter.addListener( + InstabugConstants.NETWORK_DATA_OBFUSCATION_HANDLER_EVENT, + async data => { + try { + const newData = await handler(data); + if (Platform.OS === 'android') { + Instabug.networkLog(JSON.stringify(newData)); + } else { + Instabug.networkLog(newData); + } + } catch (e) { + console.error(e); + } + } + ); + }, + + + /** + * Omit requests from being logged based on either their request or response details + * @param {string} expression + */ + setRequestFilterExpression(expression) { + _requestFilterExpression = expression; + }, + + /** + * Returns progress in terms of totalBytesSent and totalBytesExpectedToSend a network request. + * @param {function} handler + */ + setProgressHandlerForRequest(handler) { + xhr.setOnProgressCallback(handler); + }, + + +}; diff --git a/package.json b/package.json index a457f1631..5f9343200 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "instabug-reactnative", - "version": "8.2.5", + "version": "8.2.6", "description": "React Native plugin for integrating the Instabug SDK", "main": "index.js", "types": "index.d.ts", diff --git a/unlink_bridge.js b/unlink_bridge.js index f8c87b35a..11a4cd97d 100644 --- a/unlink_bridge.js +++ b/unlink_bridge.js @@ -1,2 +1,3 @@ const exec = require('child_process').exec; +require('./unlink_gradle'); exec("ruby ./node_modules/instabug-reactnative/unlink.rb || echo \"Ruby doesn't exist, if you're building this for Android only, then feel free to ignore this error, otherwise please install Ruby and run 'react-native link instabug-reactnative' again\""); diff --git a/unlink_gradle.js b/unlink_gradle.js new file mode 100644 index 000000000..1d1aac5aa --- /dev/null +++ b/unlink_gradle.js @@ -0,0 +1,97 @@ +'use strict'; +var fs = require('fs'); + +const LOG_LEVEL_SUCCESS = 0; +const LOG_LEVEL_WARN = 1; + +const CHAR_OPEN_PARAN = '{'; +const CHAR_CLOSED_PARAN = '}'; + +const GRADLE_FILE_PATH = 'android/build.gradle'; +const MAVEN_REPO_URL = + 'https://sdks.instabug.com/nexus/repository/instabug-cp'; + +function readFile(filePath, success) { + fs.readFile(filePath, 'utf-8', function(err, data) { + if (err) { + finish( + LOG_LEVEL_WARN, + `Linking process could not be completed because of\n${err.message}` + ); + } + success(data); + }); +} + +function findClosureStart(data, index) { + const alphRegex = /[a-z]/; + let maven = ''; + let foundOpenParan = false; + let closureStart = -1; + for (let i = index; i >= 0; i--) { + if (!foundOpenParan) { + foundOpenParan = data.charAt(i) === CHAR_OPEN_PARAN; + } + + if (alphRegex.test(data.charAt(i)) && foundOpenParan) { + maven = data.charAt(i) + maven; + } + if (maven === 'maven') { + closureStart = i; + break; + } + } + return closureStart; +} + +function findClosureEnd(data, index) { + const startIndex = index + MAVEN_REPO_URL.length + 2; + let closureEnd = -1; + //after + for (let i = startIndex; i < data.length; i++) { + if (data.charAt(i) === CHAR_CLOSED_PARAN) { + closureEnd = i; + break; + } + } + return closureEnd; +} + +function removeMavenRepo(data) { + const regex = /\"https:\/\/oss.sonatype.org\/content\/repositories\/snapshots\"/; + if (!regex.test(data)) { + finish(LOG_LEVEL_SUCCESS, 'Already Unlinked'); + } + const start = findClosureStart(data, data.match(regex).index); + const end = findClosureEnd(data, data.match(regex).index); + let newGradle = + data.substring(0, start) + data.substring(end + 1, data.length); + return newGradle; +} + +function writeFile(data) { + fs.writeFile(GRADLE_FILE_PATH, data, err => { + if (err) { + finish( + LOG_LEVEL_WARN, + `Unlinking process could not be completed because of\n${err.message}` + ); + } + finish(LOG_LEVEL_SUCCESS, 'Unlinking process completed successfully'); + }); +} + +function finish(logLevel, message) { + if (logLevel === LOG_LEVEL_SUCCESS) { + console.info(message); + } else { + console.warn(message); + } + + process.exit(0); +} + +readFile(GRADLE_FILE_PATH, function(data) { + const newGradle = removeMavenRepo(data); + writeFile(newGradle); +}); diff --git a/utils/InstabugConstants.js b/utils/InstabugConstants.js new file mode 100644 index 000000000..f19ffb317 --- /dev/null +++ b/utils/InstabugConstants.js @@ -0,0 +1,3 @@ +export default { + NETWORK_DATA_OBFUSCATION_HANDLER_EVENT: 'IBGSetNetworkDataObfuscationHandler', +}; \ No newline at end of file diff --git a/utils/XhrNetworkInterceptor.js b/utils/XhrNetworkInterceptor.js new file mode 100644 index 000000000..84e7f2779 --- /dev/null +++ b/utils/XhrNetworkInterceptor.js @@ -0,0 +1,138 @@ +'use strict'; + +const XMLHttpRequest = global.XMLHttpRequest; +const originalXHROpen = XMLHttpRequest.prototype.open; +const originalXHRSend = XMLHttpRequest.prototype.send; +const originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; + +var onProgressCallback; +var onDoneCallback; +var isInterceptorEnabled = false; +var network; +var duration = 0; + + +const _reset = () => { + network = { + url: '', + requestBody: '', + requestHeaders: '', + method: '', + responseBody: '', + responseCode: undefined, + responseHeaders: '', + contentType: '', + duration: 0 + }; +} + +const XHRInterceptor = { + setOnDoneCallback(callback) { + onDoneCallback = callback; + }, + setOnProgressCallback(callback) { + onProgressCallback = callback; + }, + enableInterception() { + XMLHttpRequest.prototype.open = function(method, url) { + _reset(); + network.url = url; + network.method = method; + originalXHROpen.apply(this, arguments); + }; + + XMLHttpRequest.prototype.setRequestHeader = function(header, value) { + if (network.requestHeaders === '') { + network.requestHeaders = {}; + } + network.requestHeaders[header] = value; + originalXHRSetRequestHeader.apply(this, arguments); + }; + + XMLHttpRequest.prototype.send = function(data) { + network.requestBody = data ? data : ''; + + if (this.addEventListener) { + this.addEventListener( + 'readystatechange', + async () => { + if (!isInterceptorEnabled) { + return; + } + if (this.readyState === this.HEADERS_RECEIVED) { + const contentTypeString = this.getResponseHeader('Content-Type'); + if (contentTypeString) { + network.contentType = contentTypeString.split(';')[0]; + } + + + if (this.getAllResponseHeaders()) { + const responseHeaders = this.getAllResponseHeaders().split('\r\n'); + const responseHeadersDictionary = {}; + responseHeaders.forEach(element => { + const key = element.split(':')[0]; + const value = element.split(':')[1]; + responseHeadersDictionary[key] = value; + }); + network.responseHeaders = responseHeadersDictionary; + } + + + } + if (this.readyState === this.DONE) { + duration = (Date.now() - duration); + network.responseCode = this.status; + network.duration = duration; + + if (this.response) { + if (this.responseType === 'blob') { + var responseText = await (new Response(this.response)).text(); + network.responseBody = responseText; + } else if (this.responseType === 'text') { + network.responseBody = this.response; + } + } + + if (this._hasError) { + network.requestBody = this._response; + } + if (onDoneCallback) { + onDoneCallback(network); + } + } + }, + false + ); + + const downloadUploadProgressCallback = event => { + if (!isInterceptorEnabled) { + return; + } + // check if will be able to compute progress + if (event.lengthComputable && onProgressCallback) { + let totalBytesSent = event.loaded; + let totalBytesExpectedToSend = event.total - event.loaded; + onProgressCallback(totalBytesSent, totalBytesExpectedToSend); + } + }; + this.addEventListener('progress', downloadUploadProgressCallback); + this.upload.addEventListener('progress', downloadUploadProgressCallback); + } + + duration = Date.now(); + originalXHRSend.apply(this, arguments); + }; + isInterceptorEnabled = true; + }, + + disableInterception() { + isInterceptorEnabled = false; + XMLHttpRequest.prototype.send = originalXHRSend; + XMLHttpRequest.prototype.open = originalXHROpen; + XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader; + onDoneCallback = null; + onProgressCallback = null; + } +}; + +module.exports = XHRInterceptor;