
On-device devtools capturing and inspecting push notification payloads with interactive tree JSON view, real-time search, edit-and-replay, sharing/export, persistent logs, and zero-footprint no-op production variant.
Notification Inspector is a lightweight, pure Kotlin Multiplatform (KMP) on-device DevTools for push notifications, designed to capture, log, and inspect push notification payloads directly on-device. Supporting Android, iOS, JVM (Desktop), JavaScript, and WebAssembly, it provides developers with an interactive, on-device UI console to monitor and audit Firebase Cloud Messages (FCM) or Apple Push Notifications (APNs) in real-time, greatly accelerating the development and QA cycle.
| Platform | Support Status | Payload Type | Features |
|---|---|---|---|
| Android (API 21+) | ✅ Fully Implemented |
RemoteMessage (FCM) |
Auto-interception, Standalone Activity UI, Room SQLite DB |
| iOS (iOS 12+) | ✅ Fully Implemented |
NSDictionary (APNs) |
Auto-interception, Embeddable SwiftUI Component, Room SQLite DB |
| JVM (Desktop) | 🚧 Work In Progress | Any |
Coming Soon (no-op stub) |
| JavaScript (JS) | 🚧 Work In Progress | Any |
Coming Soon (no-op stub) |
| WebAssembly (Wasm) | 🚧 Work In Progress | Any |
Coming Soon (no-op stub) |
Debugging push notifications is historically a tedious process. Developers typically have to search through verbose IDE system logs (Logcat/Xcode console), set up complex local proxy tools (Charles, Proxyman), or rely on backend logs to verify that payload properties are being delivered correctly.
Moreover, keeping debugging code and log history in production builds poses significant security risks.
Notification Inspector solves this elegantly by providing:
RemoteMessage on Android, NSDictionary on iOS) to automatically extract and format payloads.shared-no-op library variant is provided. By swapping dependencies in production, all database code, notification interceptors, and UI components are completely stripped out, achieving zero overhead and total security.RemoteMessage on Android, NSDictionary on iOS) to capture incoming notifications automatically.FirebaseMessagingService or APNs delegate) without needing backend server triggers or FCM Console.shared-no-op library variant that strips database code, interceptors, and UI components in production builds.Add the dependency to your shared/common module's build.gradle.kts file:
sourceSets {
commonMain.dependencies {
// Debug configuration uses the full inspector library
implementation("io.github.srjranjan:shared:1.0.13")
}
}To automatically isolate the inspector to development builds and use the safe no-op implementation in production, declare the dependencies conditionally inside your Android application module's build.gradle.kts:
dependencies {
// Standard debug builds contain the inspector UI and Room DB
debugImplementation("io.github.srjranjan:shared:1.0.13")
// Release builds compile against the empty no-op variant
releaseImplementation("io.github.srjranjan:shared-no-op:1.0.13")
}For native iOS apps or when linking the shared framework directly via Swift Package Manager in Xcode:
https://github.com/srjranjan/Notification-Inspector
main branch).Create an instance of NotificationInspector by passing a PlatformContext. On Android, this requires the application/activity context, whereas on other platforms it requires no arguments:
import com.srj.notificationinspector.NotificationInspector
import com.srj.notificationinspector.PlatformContext
// Android Initialization (typically in Application, Activity, or Service)
val platformContext = PlatformContext(androidContext)
val inspector = NotificationInspector(platformContext)
// iOS / JVM / Desktop Initialization
val platformContext = PlatformContext()
val inspector = NotificationInspector(platformContext)To automatically record incoming notifications, capture them inside your FirebaseMessagingService:
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class MyFirebaseMessagingService : FirebaseMessagingService() {
private lateinit var inspector: NotificationInspector
override fun onCreate() {
super.onCreate()
inspector = NotificationInspector(PlatformContext(applicationContext))
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// Captures, parses, and logs the payload
inspector.capture(remoteMessage)
// Handle your message rendering here...
}
}Launch the standalone Inspector console Activity from anywhere in your debug menu or shake detector:
// Launches InspectorActivity which hosts the Compose Multiplatform UI
inspector.launch()If you want to host the logs UI inside your own custom settings or debug Composable, use NotificationInspectorApp directly:
import com.srj.notificationinspector.ui.NotificationInspectorApp
import com.srj.notificationinspector.getNotificationRepository
@Composable
fun DebugConsoleScreen(context: PlatformContext) {
val repository = remember(context) { getNotificationRepository(context) }
NotificationInspectorApp(
repository = repository,
onClose = { /* Handle navigation back */ }
)
}On iOS, push notification payloads are received as NSDictionary. You can intercept them inside your AppDelegate and display the interface in SwiftUI.
Add payload interception to your UNUserNotificationCenterDelegate implementation:
import Shared
import UserNotifications
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
private let inspector = NotificationInspector(context: PlatformContext())
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
// Convert APNs userInfo payload to NSDictionary
let userInfo = response.notification.request.content.userInfo as NSDictionary
// Record and parse payload
inspector.capture(message: userInfo)
completionHandler()
}
}Wrap the exported Compose View Controller inside UIViewControllerRepresentable and display it in a sheet or navigation view:
import SwiftUI
import Shared
// Wrap the Compose controller for SwiftUI
struct NotificationInspectorView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
// MainViewController is generated by the shared KMP library
return MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
// Host it in your debug settings
struct DebugSettingsMenu: View {
@State private var isInspectorOpen = false
var body: some View {
VStack {
Button(action: { isInspectorOpen = true }) {
Label("Open Notification Inspector", systemImage: "bell.badge.fill")
.font(.headline)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
.sheet(isPresented: $isInspectorOpen) {
NotificationInspectorView()
.edgesIgnoringSafeArea(.all)
}
}
}The Replay feature allows you to simulate native push notification delivery locally on your device using captured or modified payloads.
Unlike a simple notification banner re-display, this feature acts as a local push injector. It programmatically re-delivers the raw FCM (Android) or APNs (iOS) payload directly into your app's platform entry points (onMessageReceived / didReceiveRemoteNotification). This lets your application's existing, unmodified push-handling code (deep links, navigation routing, custom data processing) execute naturally without needing a backend server or FCM Console.
FirebaseMessagingService and broadcasts a local com.google.android.c2dm.intent.RECEIVE intent. This triggers onMessageReceived(RemoteMessage) programmatically within the same process without root or ADB.NSDictionary payload and invokes your App Delegate's didReceiveRemoteNotification using Objective-C runtime inspection.Want to test how your app handles a different deep link, missing payload field, or updated JSON schema without triggering a new push notification from your backend server?
The Edit Payload feature lets you tweak notification parameters on-device before replaying:
Quickly share payloads with backend teams or log them during testing:
If you need to perform specific debug logic when a replay is triggered, you can register a global listener:
import com.srj.notificationinspector.NotificationInspector
// Register in your Application class or MainActivity
NotificationInspector.replayListener = { log ->
println("Manual replay triggered for log ID: ${log.id}")
}We welcome contributions of all types! Whether you are reporting a bug, suggesting a new feature, or submitting code changes, please read our Contributing Guidelines to get started.
This project is licensed under the Apache License, Version 2.0. See the LICENSE file for details.
Notification Inspector is a lightweight, pure Kotlin Multiplatform (KMP) on-device DevTools for push notifications, designed to capture, log, and inspect push notification payloads directly on-device. Supporting Android, iOS, JVM (Desktop), JavaScript, and WebAssembly, it provides developers with an interactive, on-device UI console to monitor and audit Firebase Cloud Messages (FCM) or Apple Push Notifications (APNs) in real-time, greatly accelerating the development and QA cycle.
| Platform | Support Status | Payload Type | Features |
|---|---|---|---|
| Android (API 21+) | ✅ Fully Implemented |
RemoteMessage (FCM) |
Auto-interception, Standalone Activity UI, Room SQLite DB |
| iOS (iOS 12+) | ✅ Fully Implemented |
NSDictionary (APNs) |
Auto-interception, Embeddable SwiftUI Component, Room SQLite DB |
| JVM (Desktop) | 🚧 Work In Progress | Any |
Coming Soon (no-op stub) |
| JavaScript (JS) | 🚧 Work In Progress | Any |
Coming Soon (no-op stub) |
| WebAssembly (Wasm) | 🚧 Work In Progress | Any |
Coming Soon (no-op stub) |
Debugging push notifications is historically a tedious process. Developers typically have to search through verbose IDE system logs (Logcat/Xcode console), set up complex local proxy tools (Charles, Proxyman), or rely on backend logs to verify that payload properties are being delivered correctly.
Moreover, keeping debugging code and log history in production builds poses significant security risks.
Notification Inspector solves this elegantly by providing:
RemoteMessage on Android, NSDictionary on iOS) to automatically extract and format payloads.shared-no-op library variant is provided. By swapping dependencies in production, all database code, notification interceptors, and UI components are completely stripped out, achieving zero overhead and total security.RemoteMessage on Android, NSDictionary on iOS) to capture incoming notifications automatically.FirebaseMessagingService or APNs delegate) without needing backend server triggers or FCM Console.shared-no-op library variant that strips database code, interceptors, and UI components in production builds.Add the dependency to your shared/common module's build.gradle.kts file:
sourceSets {
commonMain.dependencies {
// Debug configuration uses the full inspector library
implementation("io.github.srjranjan:shared:1.0.13")
}
}To automatically isolate the inspector to development builds and use the safe no-op implementation in production, declare the dependencies conditionally inside your Android application module's build.gradle.kts:
dependencies {
// Standard debug builds contain the inspector UI and Room DB
debugImplementation("io.github.srjranjan:shared:1.0.13")
// Release builds compile against the empty no-op variant
releaseImplementation("io.github.srjranjan:shared-no-op:1.0.13")
}For native iOS apps or when linking the shared framework directly via Swift Package Manager in Xcode:
https://github.com/srjranjan/Notification-Inspector
main branch).Create an instance of NotificationInspector by passing a PlatformContext. On Android, this requires the application/activity context, whereas on other platforms it requires no arguments:
import com.srj.notificationinspector.NotificationInspector
import com.srj.notificationinspector.PlatformContext
// Android Initialization (typically in Application, Activity, or Service)
val platformContext = PlatformContext(androidContext)
val inspector = NotificationInspector(platformContext)
// iOS / JVM / Desktop Initialization
val platformContext = PlatformContext()
val inspector = NotificationInspector(platformContext)To automatically record incoming notifications, capture them inside your FirebaseMessagingService:
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class MyFirebaseMessagingService : FirebaseMessagingService() {
private lateinit var inspector: NotificationInspector
override fun onCreate() {
super.onCreate()
inspector = NotificationInspector(PlatformContext(applicationContext))
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
// Captures, parses, and logs the payload
inspector.capture(remoteMessage)
// Handle your message rendering here...
}
}Launch the standalone Inspector console Activity from anywhere in your debug menu or shake detector:
// Launches InspectorActivity which hosts the Compose Multiplatform UI
inspector.launch()If you want to host the logs UI inside your own custom settings or debug Composable, use NotificationInspectorApp directly:
import com.srj.notificationinspector.ui.NotificationInspectorApp
import com.srj.notificationinspector.getNotificationRepository
@Composable
fun DebugConsoleScreen(context: PlatformContext) {
val repository = remember(context) { getNotificationRepository(context) }
NotificationInspectorApp(
repository = repository,
onClose = { /* Handle navigation back */ }
)
}On iOS, push notification payloads are received as NSDictionary. You can intercept them inside your AppDelegate and display the interface in SwiftUI.
Add payload interception to your UNUserNotificationCenterDelegate implementation:
import Shared
import UserNotifications
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
private let inspector = NotificationInspector(context: PlatformContext())
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
// Convert APNs userInfo payload to NSDictionary
let userInfo = response.notification.request.content.userInfo as NSDictionary
// Record and parse payload
inspector.capture(message: userInfo)
completionHandler()
}
}Wrap the exported Compose View Controller inside UIViewControllerRepresentable and display it in a sheet or navigation view:
import SwiftUI
import Shared
// Wrap the Compose controller for SwiftUI
struct NotificationInspectorView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
// MainViewController is generated by the shared KMP library
return MainViewControllerKt.MainViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
// Host it in your debug settings
struct DebugSettingsMenu: View {
@State private var isInspectorOpen = false
var body: some View {
VStack {
Button(action: { isInspectorOpen = true }) {
Label("Open Notification Inspector", systemImage: "bell.badge.fill")
.font(.headline)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
.sheet(isPresented: $isInspectorOpen) {
NotificationInspectorView()
.edgesIgnoringSafeArea(.all)
}
}
}The Replay feature allows you to simulate native push notification delivery locally on your device using captured or modified payloads.
Unlike a simple notification banner re-display, this feature acts as a local push injector. It programmatically re-delivers the raw FCM (Android) or APNs (iOS) payload directly into your app's platform entry points (onMessageReceived / didReceiveRemoteNotification). This lets your application's existing, unmodified push-handling code (deep links, navigation routing, custom data processing) execute naturally without needing a backend server or FCM Console.
FirebaseMessagingService and broadcasts a local com.google.android.c2dm.intent.RECEIVE intent. This triggers onMessageReceived(RemoteMessage) programmatically within the same process without root or ADB.NSDictionary payload and invokes your App Delegate's didReceiveRemoteNotification using Objective-C runtime inspection.Want to test how your app handles a different deep link, missing payload field, or updated JSON schema without triggering a new push notification from your backend server?
The Edit Payload feature lets you tweak notification parameters on-device before replaying:
Quickly share payloads with backend teams or log them during testing:
If you need to perform specific debug logic when a replay is triggered, you can register a global listener:
import com.srj.notificationinspector.NotificationInspector
// Register in your Application class or MainActivity
NotificationInspector.replayListener = { log ->
println("Manual replay triggered for log ID: ${log.id}")
}We welcome contributions of all types! Whether you are reporting a bug, suggesting a new feature, or submitting code changes, please read our Contributing Guidelines to get started.
This project is licensed under the Apache License, Version 2.0. See the LICENSE file for details.