Snag is a native network debugger for iOS and Android. No proxies, no certificates, and zero configuration required. It uses Bonjour for automatic discovery, allowing you to monitor network traffic in real-time on a desktop viewer over your local network.
- Clone the repository.
- Open
mac/Snag.xcodeprojin Xcode. - Build and Run.
Prerequisites:
ghinstalled and authenticated (gh auth login)- Xcode command line tools installed
Commands:
chmod +x scripts/release_macos_dmg.sh
./scripts/release_macos_dmg.shThis builds Snag.app (Release, signing disabled), creates dist/Snag_<version>.dmg, then uploads it to the GitHub Release tag v<version> (derived from mac/Snag/Info.plist).
Add the package URL to your project:
https://github.com/thanhcuong1990/Snag.git
pod 'Snag', '~> 1.0.0'Initialize Snag in your AppDelegate or at the start of your app:
import Snag
#if DEBUG
Snag.start()
#endifAdd the following to your app's Info.plist to allow Bonjour discovery on a real device:
<key>NSLocalNetworkUsageDescription</key>
<string>This app uses the local network to discover nearby devices using Bonjour.</string>
<key>NSBonjourServices</key>
<array>
<string>_Snag._tcp</string>
</array>let config = SnagConfiguration()
config.project?.name = "My App"
config.device?.name = "Developer iPhone"
Snag.start(configuration: config)Add the dependency to your build.gradle:
implementation 'io.github.thanhcuong1990:snag:1.0.4'For detailed instructions on how to publish this package yourself, see android/publishing.md.
Start the client and add the OkHttp interceptor:
Important: Only enable Snag in non-production builds (e.g. debug/staging). Do not initialize Snag or attach the interceptor in release/production.
In the snippets below, BuildConfig refers to your app module BuildConfig.
Option A: Simple Android setup (you control the OkHttpClient)
import android.content.Context
import com.snag.Snag
fun initSnagIfNonProd(context: Context) {
if (BuildConfig.DEBUG || BuildConfig.FLAVOR == "staging") {
Snag.start(context.applicationContext)
}
}import com.snag.SnagInterceptor
import okhttp3.OkHttpClient
fun buildOkHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
if (BuildConfig.DEBUG || BuildConfig.FLAVOR == "staging") {
builder.addInterceptor(SnagInterceptor.getInstance())
}
return builder.build()
}Option B: React Native setup (OkHttpClientProvider + reflection-safe initializer)
Create NetworkDebugInitializer.kt (for example: android/app/src/main/java/your/package/name/NetworkDebugInitializer.kt)
package your.package.name
import android.content.Context
import android.util.Log
import com.facebook.react.modules.network.OkHttpClientProvider
import okhttp3.Interceptor
import okhttp3.OkHttpClient
object NetworkDebugInitializer {
private const val TAG = "NetworkDebugInitializer"
private const val SNAG_CLASS = "com.snag.Snag"
private const val SNAG_INTERCEPTOR_CLASS = "com.snag.SnagInterceptor"
private val isDebugOrStaging: Boolean
get() = BuildConfig.DEBUG || BuildConfig.FLAVOR == "staging"
fun initForReactNative(context: Context, existingBuilder: OkHttpClient.Builder? = null): Boolean {
if (!isDebugOrStaging || !isAvailable()) return false
startIfAvailable(context)
configureOkHttpClientProvider(context, existingBuilder)
return true
}
fun isAvailable(): Boolean =
try {
Class.forName(SNAG_CLASS)
Class.forName(SNAG_INTERCEPTOR_CLASS)
true
} catch (_: Throwable) {
false
}
private fun startIfAvailable(context: Context) =
runCatching {
val snagClass = Class.forName(SNAG_CLASS)
val startMethod = snagClass.getMethod("start", Context::class.java)
startMethod.invoke(null, context)
}
.onFailure { Log.d(TAG, "Snag.start() not available: ${it.message}") }
private fun configureOkHttpClientProvider(
context: Context,
existingBuilder: OkHttpClient.Builder? = null,
) {
val appContext = context.applicationContext
OkHttpClientProvider.setOkHttpClientFactory {
val builder = existingBuilder ?: OkHttpClientProvider.createClientBuilder(appContext)
addSnagInterceptorIfAvailable(builder)
builder.build()
}
}
private fun addSnagInterceptorIfAvailable(builder: OkHttpClient.Builder) {
if (builder.interceptors().any { it.javaClass.name == SNAG_INTERCEPTOR_CLASS }) return
runCatching {
val interceptorClass = Class.forName(SNAG_INTERCEPTOR_CLASS)
val getInstanceMethod = interceptorClass.getMethod("getInstance")
val interceptor = getInstanceMethod.invoke(null) as? Interceptor
interceptor?.let(builder::addInterceptor) ?: Log.d(TAG, "SnagInterceptor instance is null")
}
.onFailure { Log.d(TAG, "SnagInterceptor not available: ${it.message}") }
}
}Call it from your MainApplication.kt:
override fun onCreate() {
super.onCreate()
NetworkDebugInitializer.initForReactNative(applicationContext)
}MIT

