Skip to content

Commit

Permalink
[go] add network inspector support (expo#22741)
Browse files Browse the repository at this point in the history
# Why

add network inspector support for expo go
close ENG-8010

# How

integrate the ExpoRequestCdpInterceptor from expo-modules-core. unlike expo-dev-launcher to use hacky solution, we want to enable the network inspector on release mode, so the implementation follows formal way.
- on ios, we leverage the `RCTSetCustomNSURLSessionConfigurationProvider` from react-native to create dedicated `URLSessionConfiguration`.
- on android, we already have a dedicated okhttp client from expo go. this pr just adds the interceptors.
  - found image requests are not intercepted, it is because we don't use okhttp for fresco image pipeline. this pr tries to use the okhttp for fresco as react-native.
  - android expo go has multiple react instances. however, the network inspector currently only support single inspector target to metro-inspector-proxy. as the result, we only limit the current foreground activity to send network inspector events.

# Test Plan

ncl + expo go to test the network inspector
  • Loading branch information
Kudo authored Jun 21, 2023
1 parent faf0cf8 commit 60ad5af
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package host.exp.exponent
import android.util.Log
import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.common.JavascriptException
import expo.modules.kotlin.devtools.ExpoNetworkInspectOkHttpAppInterceptor
import expo.modules.kotlin.devtools.ExpoNetworkInspectOkHttpNetworkInterceptor
import host.exp.exponent.network.ExponentNetwork
import host.exp.expoview.Exponent
import okhttp3.CookieJar
Expand Down Expand Up @@ -172,6 +174,8 @@ object ReactNativeStaticHelpers {
.writeTimeout(0, TimeUnit.MILLISECONDS)
.cookieJar(cookieJar as CookieJar)
.cache(exponentNetwork!!.cache)
.addInterceptor(ExpoNetworkInspectOkHttpAppInterceptor())
.addNetworkInterceptor(ExpoNetworkInspectOkHttpNetworkInterceptor())
return client.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ abstract class ReactNativeActivity :
RNObject("com.facebook.react.ReactInstanceManager")
protected var isCrashed = false

protected val networkInterceptor =
RNObject("host.exp.exponent.ExpoNetworkInterceptor")

protected var manifestUrl: String? = null
var experienceKey: ExperienceKey? = null
protected var sdkVersion: String? = null
Expand Down Expand Up @@ -287,6 +290,7 @@ abstract class ReactNativeActivity :
override fun onPause() {
super.onPause()
if (reactInstanceManager.isNotNull && !isCrashed) {
networkInterceptor.call("onPause")
reactInstanceManager.onHostPause()
// TODO: use onHostPause(activity)
}
Expand All @@ -296,6 +300,7 @@ abstract class ReactNativeActivity :
super.onResume()
if (reactInstanceManager.isNotNull && !isCrashed) {
reactInstanceManager.onHostResume(this, this)
networkInterceptor.call("onResume", reactInstanceManager.get())
}
}

Expand Down Expand Up @@ -487,6 +492,8 @@ abstract class ReactNativeActivity :
initialProps(bundle)
)

networkInterceptor.loadVersion(sdkVersion).construct().call("start", manifest, mReactInstanceManager.get())

// Requesting layout to make sure {@link ReactRootView} attached to {@link ReactInstanceManager}
// Otherwise, {@link ReactRootView} will hang in {@link waitForReactRootViewToHaveChildrenAndRunCallback}.
// Originally react-native will automatically attach after `startReactApplication`.
Expand Down
15 changes: 14 additions & 1 deletion android/expoview/src/main/java/host/exp/expoview/Exponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ import android.os.StrictMode.ThreadPolicy
import android.os.UserManager
import com.facebook.common.internal.ByteStreams
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.producers.HttpUrlConnectionNetworkFetcher
import com.raizlabs.android.dbflow.config.DatabaseConfig
import com.raizlabs.android.dbflow.config.FlowConfig
import com.raizlabs.android.dbflow.config.FlowManager
import expo.modules.core.interfaces.Package
import expo.modules.core.interfaces.SingletonModule
import expo.modules.kotlin.devtools.ExpoNetworkInspectOkHttpAppInterceptor
import expo.modules.kotlin.devtools.ExpoNetworkInspectOkHttpNetworkInterceptor
import expo.modules.manifests.core.Manifest
import host.exp.exponent.*
import host.exp.exponent.analytics.EXL
Expand All @@ -40,6 +44,7 @@ import versioned.host.exp.exponent.ExponentPackageDelegate
import java.io.*
import java.net.URLEncoder
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import javax.inject.Inject

class Exponent private constructor(val context: Context, val application: Application) {
Expand Down Expand Up @@ -407,7 +412,15 @@ class Exponent private constructor(val context: Context, val application: Applic
}

try {
Fresco.initialize(context)
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(HttpUrlConnectionNetworkFetcher.HTTP_DEFAULT_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
.readTimeout(0, TimeUnit.MILLISECONDS)
.writeTimeout(0, TimeUnit.MILLISECONDS)
.addInterceptor(ExpoNetworkInspectOkHttpAppInterceptor())
.addNetworkInterceptor(ExpoNetworkInspectOkHttpNetworkInterceptor())
.build()
val imagePipelineConfig = OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient).build()
Fresco.initialize(context, imagePipelineConfig)
} catch (e: RuntimeException) {
EXL.testError(e)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2015-present 650 Industries. All rights reserved.

package versioned.host.exp.exponent

import com.facebook.react.ReactInstanceManager
import com.facebook.react.bridge.Inspector
import com.facebook.react.devsupport.DevServerHelper
import com.facebook.react.devsupport.DevSupportManagerBase
import com.facebook.react.devsupport.InspectorPackagerConnection
import expo.modules.kotlin.devtools.ExpoRequestCdpInterceptor
import expo.modules.manifests.core.Manifest
import java.io.Closeable

@Suppress("unused")
class ExpoNetworkInterceptor : Closeable, ExpoRequestCdpInterceptor.Delegate {
private var isStarted = false
private val inspectorPackagerConnection = InspectorPackagerConnectionWrapper()
private var reactInstanceManager: ReactInstanceManager? = null

fun start(manifest: Manifest, reactInstanceManager: ReactInstanceManager) {
val buildProps = (manifest?.getPluginProperties("expo-build-properties")?.get("android") as? Map<*, *>)
?.mapKeys { it.key.toString() }
val enableNetworkInspector = buildProps?.get("networkInspector") as? Boolean ?: true
isStarted = enableNetworkInspector

this.onResume(reactInstanceManager)
}

fun onResume(reactInstanceManager: ReactInstanceManager) {
if (!isStarted) {
return
}
this.reactInstanceManager = reactInstanceManager
ExpoRequestCdpInterceptor.setDelegate(this)
}

fun onPause() {
if (!isStarted) {
return
}
ExpoRequestCdpInterceptor.setDelegate(null)
this.reactInstanceManager = null
}

override fun close() {
this.onPause()
}

override fun dispatch(event: String) {
reactInstanceManager?.let {
inspectorPackagerConnection.sendWrappedEventToAllPages(it, event)
}
}
}

/**
* A `InspectorPackagerConnection` wrapper to expose private members with reflection
*/
internal class InspectorPackagerConnectionWrapper {
private val devServerHelperField = DevSupportManagerBase::class.java.getDeclaredField("mDevServerHelper")
private val inspectorPackagerConnectionField = DevServerHelper::class.java.getDeclaredField("mInspectorPackagerConnection")
private val sendWrappedEventMethod = InspectorPackagerConnection::class.java.getDeclaredMethod("sendWrappedEvent", String::class.java, String::class.java)

init {
devServerHelperField.isAccessible = true
inspectorPackagerConnectionField.isAccessible = true
sendWrappedEventMethod.isAccessible = true
}

fun sendWrappedEventToAllPages(reactInstanceManager: ReactInstanceManager, event: String) {
val devServerHelper = devServerHelperField[reactInstanceManager.devSupportManager]
val inspectorPackagerConnection = inspectorPackagerConnectionField[devServerHelper] as? InspectorPackagerConnection
for (page in Inspector.getPages()) {
if (!page.title.contains("Reanimated")) {
sendWrappedEventMethod.invoke(inspectorPackagerConnection, page.id.toString(), event)
}
}
}
}
8 changes: 8 additions & 0 deletions ios/Exponent.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions ios/Exponent/Versioned/Core/EXVersionManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#import "EXDisabledDevLoadingView.h"
#import "EXDisabledDevMenu.h"
#import "EXDisabledRedBox.h"
#import "EXVersionedNetworkInterceptor.h"
#import "EXVersionManager.h"
#import "EXScopedBridgeModule.h"
#import "EXStatusBarManager.h"
Expand Down Expand Up @@ -96,6 +97,7 @@ @interface EXVersionManager () <RCTTurboModuleManagerDelegate>
@property (nonatomic, strong) NSDictionary *params;
@property (nonatomic, strong) EXManifestsManifest *manifest;
@property (nonatomic, strong) RCTTurboModuleManager *turboModuleManager;
@property (nonatomic, strong) EXVersionedNetworkInterceptor *networkInterceptor;

@end

Expand Down Expand Up @@ -143,7 +145,13 @@ - (void)bridgeWillStartLoading:(id)bridge
NSURL *bundleURL = [bridge bundleURL];
NSString *packagerServerHostPort = [NSString stringWithFormat:@"%@:%@", bundleURL.host, bundleURL.port];
[[RCTPackagerConnection sharedPackagerConnection] reconnect:packagerServerHostPort];
[RCTInspectorDevServerHelper connectWithBundleURL:bundleURL];
RCTInspectorPackagerConnection *inspectorPackagerConnection = [RCTInspectorDevServerHelper connectWithBundleURL:bundleURL];

NSDictionary<NSString *, id> *buildProps = [self.manifest getPluginPropertiesWithPackageName:@"expo-build-properties"];
NSNumber *enableNetworkInterceptor = [[buildProps objectForKey:@"ios"] objectForKey:@"unstable_networkInspector"];
if (enableNetworkInterceptor == nil || [enableNetworkInterceptor boolValue] != NO) {
self.networkInterceptor = [[EXVersionedNetworkInterceptor alloc] initWithRCTInspectorPackagerConnection:inspectorPackagerConnection];
}
}

// Manually send a "start loading" notif, since the real one happened uselessly inside the RCTBatchedBridge constructor
Expand All @@ -161,7 +169,9 @@ - (void)bridgeFinishedLoading:(id)bridge
}];
}

- (void)invalidate {}
- (void)invalidate {
self.networkInterceptor = nil;
}

- (NSDictionary<NSString *, NSString *> *)devMenuItemsForBridge:(id)bridge
{
Expand Down
15 changes: 15 additions & 0 deletions ios/Exponent/Versioned/Core/EXVersionedNetworkInterceptor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2015-present 650 Industries. All rights reserved.

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class RCTInspectorPackagerConnection;

@interface EXVersionedNetworkInterceptor : NSObject

- (instancetype)initWithRCTInspectorPackagerConnection:(RCTInspectorPackagerConnection *)inspectorPackgerConnection;

@end

NS_ASSUME_NONNULL_END
110 changes: 110 additions & 0 deletions ios/Exponent/Versioned/Core/EXVersionedNetworkInterceptor.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2015-present 650 Industries. All rights reserved.

#import "EXVersionedNetworkInterceptor.h"

#import <React/RCTHTTPRequestHandler.h>
#import <React/RCTInspector.h>
#import <React/RCTInspectorPackagerConnection.h>
#import <SocketRocket/SRWebSocket.h>

#import "Expo_Go-Swift.h"
#import "ExpoModulesCore-Swift.h"

#pragma mark - RCTInspectorPackagerConnection category interface

@interface RCTInspectorPackagerConnection(sendWrappedEventToAllPages)

- (BOOL)isReadyToSend;
- (void)sendWrappedEventToAllPages:(NSString *)event;

@end

#pragma mark -

@interface EXVersionedNetworkInterceptor () <EXRequestCdpInterceptorDelegate>

@property (nonatomic, strong) RCTInspectorPackagerConnection *inspectorPackgerConnection;

@end

@implementation EXVersionedNetworkInterceptor

- (instancetype)initWithRCTInspectorPackagerConnection:(RCTInspectorPackagerConnection *)inspectorPackgerConnection
{
if (self = [super init]) {
self.inspectorPackgerConnection = inspectorPackgerConnection;
[EXRequestCdpInterceptor.shared setDelegate:self];

Class requestInterceptorClass = [EXRequestInterceptorProtocol class];
RCTSetCustomNSURLSessionConfigurationProvider(^{
NSURLSessionConfiguration *urlSessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSMutableArray<Class> *protocolClasses = [urlSessionConfiguration.protocolClasses mutableCopy];
if (![protocolClasses containsObject:requestInterceptorClass]) {
[protocolClasses insertObject:requestInterceptorClass atIndex:0];
}
urlSessionConfiguration.protocolClasses = protocolClasses;

[urlSessionConfiguration setHTTPShouldSetCookies:YES];
[urlSessionConfiguration setHTTPCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways];
[urlSessionConfiguration setHTTPCookieStorage:[NSHTTPCookieStorage sharedHTTPCookieStorage]];
return urlSessionConfiguration;
});
}
return self;
}

- (void)dealloc
{
[EXRequestCdpInterceptor.shared setDelegate:nil];
}

#pragma mark - EXRequestCdpInterceptorDelegate implementations

- (void)dispatch:(NSString * _Nonnull)event {
[self.inspectorPackgerConnection sendWrappedEventToAllPages:event];
}

@end

#pragma mark - RCTInspectorPackagerConnection category

@interface RCTInspectorPackagerConnection(sendWrappedEventToAllPages)

- (BOOL)isReadyToSend;
- (void)sendWrappedEventToAllPages:(NSString *)event;

@end

#pragma mark - RCTInspectorPackagerConnection category implementation

@implementation RCTInspectorPackagerConnection(sendWrappedEventToAllPages)

- (BOOL)isReadyToSend
{
if ([self isConnected]) {
return YES;
}

SRWebSocket *websocket = [self valueForKey:@"_webSocket"];
return websocket.readyState == SR_OPEN;
}

- (void)sendWrappedEventToAllPages:(NSString *)event
{
if (![self isReadyToSend]) {
return;
}

SEL selector = NSSelectorFromString(@"sendWrappedEvent:message:");
if ([self respondsToSelector:selector]) {
IMP sendWrappedEventIMP = [self methodForSelector:selector];
void (*functor)(id, SEL, NSString *, NSString *) = (void *)sendWrappedEventIMP;
for (RCTInspectorPage* page in RCTInspector.pages) {
if (![page.title containsString:@"Reanimated"]) {
functor(self, selector, [@(page.id) stringValue], event);
}
}
}
}

@end
3 changes: 3 additions & 0 deletions ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ abstract_target 'Expo Go' do
pod 'JKBigInteger', :podspec => 'vendored/common/JKBigInteger.podspec.json'
pod 'MBProgressHUD', '~> 1.2.0'

# transitive dependency of React-Core and we use it to get the `RCTInspectorPackagerConnection` state
pod 'SocketRocket'

# Required by firebase core versions 9.x / 10.x (included with SDK 47)
# See https://github.com/invertase/react-native-firebase/issues/6332#issuecomment-1189734581
pod 'FirebaseCore', :modular_headers => true
Expand Down
Loading

0 comments on commit 60ad5af

Please sign in to comment.