capacitor-pica-network-logger 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,45 +4,38 @@
4
4
 
5
5
  # capacitor-pica-network-logger
6
6
 
7
- Capacitor HTTP inspector with debug-only native capture and a JS wrapper that mirrors `@capacitor/http`.
7
+ A Capacitor plugin for logging and inspecting HTTP requests with native inspector UIs on iOS and Android.
8
8
 
9
9
  ![Network logger walkthrough](assets/pica_network_logger.gif)
10
10
 
11
- ## Tech stack
12
-
13
- - Capacitor 7
14
- - Swift (URLProtocol logging on iOS)
15
- - Kotlin (HttpURLConnection reflection logging on Android)
16
- - TypeScript wrapper for `@capacitor/http`
17
-
18
11
  ## Features
19
12
 
20
- - Debug-only request/response logging
21
- - Native inspector UI (standalone Activity/UIViewController)
13
+ - Request/response pair logging with automatic correlation
14
+ - Native inspector UI on iOS (UIKit) and Android (Jetpack Compose + Material 3)
22
15
  - Body text search with highlighting in the inspector
23
- - Persistent log storage across inspector sessions
24
- - Notifications to open inspector
25
- - Redaction + max body size from Capacitor config
26
- - cURL/JSON/HAR exports + copy/share
27
- - Save cURL/JSON/HAR locally
28
- - Copy all logs as HAR
16
+ - Persistent log storage (SQLite) across inspector sessions
17
+ - Responsive Android layout (compact, medium, expanded breakpoints)
18
+ - Dark/light theme toggle
19
+ - Configurable header and JSON body field redaction
20
+ - Configurable max body size with truncation
21
+ - Local notifications on each completed request (tap to open inspector)
22
+ - Export as cURL, plain text, or HAR 1.2
23
+ - No-op on web -- safe to import in any environment
29
24
 
30
25
  See the [Changelog](CHANGELOG.md) for a full history of changes.
31
26
 
32
- ## Project layout
27
+ ## Platform requirements
33
28
 
34
- - `src/`: JS wrapper and plugin typings
35
- - `android/`: Capacitor Android plugin + reflection hook
36
- - `ios/`: Capacitor iOS plugin + URLProtocol logger
37
- - `examples/sample-app/`: demo app
29
+ | Platform | Minimum version |
30
+ |---|---|
31
+ | Capacitor | 7+ |
32
+ | iOS | 14.0 |
33
+ | Android | API 23 (Android 6.0) |
38
34
 
39
35
  ## Installation
40
36
 
41
37
  ```bash
42
38
  npm install capacitor-pica-network-logger
43
- ```
44
-
45
- ```bash
46
39
  npx cap sync
47
40
  ```
48
41
 
@@ -53,71 +46,86 @@ Add to your app's `capacitor.config.ts`:
53
46
  ```ts
54
47
  plugins: {
55
48
  PicaNetworkLogger: {
56
- enabled: true,
57
- maxBodySize: 131072,
58
- notify: true,
59
- redactHeaders: ["authorization", "cookie"],
60
- redactJsonFields: ["password", "token"]
49
+ maxBodySize: 131072, // max chars per body (default: 128 KB)
50
+ notify: true, // show notifications per request (default: true)
51
+ redactHeaders: ["authorization", "cookie"], // header names to redact (default: none)
52
+ redactJsonFields: ["password", "token"] // JSON field names to redact (default: none)
61
53
  }
62
54
  }
63
55
  ```
64
56
 
57
+ | Option | Type | Default | Description |
58
+ |---|---|---|---|
59
+ | `maxBodySize` | `number` | `131072` | Maximum characters stored per request/response body. Truncated beyond this. |
60
+ | `notify` | `boolean` | `true` | Post a local notification for each completed request. On Android 13+ requests `POST_NOTIFICATIONS` permission. |
61
+ | `redactHeaders` | `string[]` | `[]` | Header names (case-insensitive) whose values are replaced with `[REDACTED]`. |
62
+ | `redactJsonFields` | `string[]` | `[]` | Top-level JSON body field names (case-insensitive) whose values are replaced with `[REDACTED]`. |
63
+
65
64
  ## Usage
66
65
 
67
- Import the wrapper and use it in place of `@capacitor/http`:
66
+ The plugin exposes three methods: `startRequest`, `finishRequest`, and `openInspector`. Use your own HTTP client (e.g. `fetch`) and wrap calls with the logging methods:
68
67
 
69
68
  ```ts
70
- import { CapacitorHttp, PicaNetworkLogger } from 'capacitor-pica-network-logger';
69
+ import { PicaNetworkLogger } from 'capacitor-pica-network-logger';
70
+
71
+ // 1. Log the start of a request
72
+ const { id } = await PicaNetworkLogger.startRequest({
73
+ method: 'GET',
74
+ url: 'https://jsonplaceholder.typicode.com/posts/1',
75
+ headers: { 'Accept': 'application/json' },
76
+ });
77
+
78
+ // 2. Make the actual HTTP call
79
+ const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
80
+ const body = await response.text();
81
+
82
+ // 3. Log the completion
83
+ await PicaNetworkLogger.finishRequest({
84
+ id,
85
+ status: response.status,
86
+ headers: Object.fromEntries(response.headers.entries()),
87
+ body,
88
+ });
71
89
 
72
- await CapacitorHttp.get({ url: 'https://example.com' });
90
+ // 4. Open the inspector UI
73
91
  await PicaNetworkLogger.openInspector();
74
92
  ```
75
93
 
76
- If you want to keep call sites unchanged, alias `@capacitor/http` to this package in your bundler.
94
+ ### API
77
95
 
78
- ### Minimal changes (existing `@capacitor/http` usage)
96
+ | Method | Signature | Description |
97
+ |---|---|---|
98
+ | `startRequest` | `(options: StartRequestOptions) => Promise<{ id: string }>` | Log the start of a request. Returns an `id` for correlation. |
99
+ | `finishRequest` | `(options: FinishRequestOptions) => Promise<void>` | Log the completion of a request (status, headers, body, error). |
100
+ | `openInspector` | `() => Promise<void>` | Open the native inspector UI. |
79
101
 
80
- If you already use `@capacitor/http`, you can keep your code as-is by adding a module alias so imports resolve to this package.
81
-
82
- **Vite**
102
+ ### Types
83
103
 
84
104
  ```ts
85
- // vite.config.ts
86
- import { defineConfig } from 'vite';
87
-
88
- export default defineConfig({
89
- resolve: {
90
- alias: {
91
- '@capacitor/http': 'capacitor-pica-network-logger'
92
- }
93
- }
94
- });
95
- ```
96
-
97
- **Webpack**
105
+ interface StartRequestOptions {
106
+ method: string;
107
+ url: string;
108
+ headers?: Record<string, string>;
109
+ body?: unknown;
110
+ }
98
111
 
99
- ```js
100
- // webpack.config.js
101
- module.exports = {
102
- resolve: {
103
- alias: {
104
- '@capacitor/http': 'capacitor-pica-network-logger'
105
- }
106
- }
107
- };
112
+ interface FinishRequestOptions {
113
+ id: string;
114
+ status?: number;
115
+ headers?: Record<string, string>;
116
+ body?: unknown;
117
+ error?: string;
118
+ }
108
119
  ```
109
120
 
110
- **TypeScript paths (editor/typecheck)**
121
+ ## Project layout
111
122
 
112
- ```json
113
- // tsconfig.json
114
- {
115
- "compilerOptions": {
116
- "paths": {
117
- "@capacitor/http": ["node_modules/capacitor-pica-network-logger"]
118
- }
119
- }
120
- }
123
+ ```
124
+ src/ TypeScript plugin registration and types
125
+ ios/Plugin/ Swift iOS plugin (UIKit inspector, SQLite storage, notifications)
126
+ android/ Kotlin Android plugin (Compose inspector, SQLite storage, notifications)
127
+ examples/ Sample Capacitor app for manual testing
128
+ scripts/ Release script
121
129
  ```
122
130
 
123
131
  ## Build
@@ -137,15 +145,26 @@ npm install
137
145
  npm run dev
138
146
  ```
139
147
 
140
- ## Modifications
148
+ Run on device:
149
+
150
+ ```bash
151
+ # iOS
152
+ cd examples/sample-app && npm run build && npx cap sync ios && npx cap run ios
153
+
154
+ # Android
155
+ cd examples/sample-app && npm run build && npx cap sync android && npx cap run android
156
+ ```
157
+
158
+ ## Release
159
+
160
+ ```bash
161
+ npm run release # prompts for patch/minor/major
162
+ npm run release -- patch # patch bump
163
+ npm run release -- 1.0.0 # explicit version
164
+ ```
141
165
 
142
- - JS wrapper: `src/httpWithLogging.ts`
143
- - Android hook: `android/src/main/java/com/linakis/capacitorpicanetworklogger/ReflectionHook.kt`
144
- - iOS logger: `ios/Plugin/InspectorURLProtocol.swift`
145
- - UI (iOS): `ios/Plugin/InspectorViewController.swift`
146
- - UI (Android): `android/src/main/java/com/linakis/capacitorpicanetworklogger/InspectorActivity.kt`
166
+ The release script updates `CHANGELOG.md`, bumps `package.json`, builds, commits, tags, pushes, publishes to npm, and creates a GitHub Release (if `gh` CLI is available).
147
167
 
148
- ## Notes
168
+ ## License
149
169
 
150
- - Android reflection hook uses `URLStreamHandlerFactory` and will fail if another library set one first.
151
- - The JS wrapper is still authoritative on Android; reflection logging is best-effort.
170
+ MIT
@@ -6,8 +6,8 @@ object LogRepositoryStore {
6
6
  private var repository: LogRepository? = null
7
7
  private var appContext: android.content.Context? = null
8
8
  private var maxBodySize: Int = 131072
9
- private var redactHeaders: Set<String> = setOf("authorization", "cookie")
10
- private var redactJsonFields: Set<String> = setOf("password", "token")
9
+ private var redactHeaders: Set<String> = emptySet()
10
+ private var redactJsonFields: Set<String> = emptySet()
11
11
  private var notifyEnabled: Boolean = true
12
12
  private val requestStartTs: MutableMap<String, Long> = mutableMapOf()
13
13
 
@@ -78,9 +78,15 @@ object LogRepositoryStore {
78
78
  data.put("id", id)
79
79
  if (status != null) data.put("status", status)
80
80
  data.put("headers", headers?.redactedHeaders()?.toJsObject())
81
- val truncated = truncate(body?.redactJson())
82
- data.put("responseBody", truncated.value)
83
- data.put("responseBodyTruncated", truncated.truncated)
81
+ val contentType = contentType(headers)
82
+ if (body != null && isBinaryContentType(contentType)) {
83
+ data.put("responseBody", binaryPlaceholder(contentType))
84
+ data.put("responseBodyTruncated", false)
85
+ } else {
86
+ val truncated = truncate(body?.redactJson())
87
+ data.put("responseBody", truncated.value)
88
+ data.put("responseBodyTruncated", truncated.truncated)
89
+ }
84
90
  if (error != null) data.put("error", error)
85
91
  if (protocol != null) data.put("protocol", protocol)
86
92
  if (ssl != null) data.put("ssl", ssl)
@@ -121,6 +127,36 @@ object LogRepositoryStore {
121
127
  }
122
128
 
123
129
  private data class Truncated(val value: String?, val truncated: Boolean)
130
+
131
+ private fun contentType(headers: Map<String, String>?): String? {
132
+ if (headers == null) return null
133
+ for ((key, value) in headers) {
134
+ if (key.lowercase() == "content-type") return value.lowercase()
135
+ }
136
+ return null
137
+ }
138
+
139
+ private val binaryPrefixes = listOf(
140
+ "image/", "video/", "audio/", "application/octet-stream",
141
+ "application/pdf", "application/zip", "application/gzip",
142
+ "font/", "application/vnd.", "application/x-protobuf"
143
+ )
144
+
145
+ private fun isBinaryContentType(contentType: String?): Boolean {
146
+ val ct = contentType ?: return false
147
+ return binaryPrefixes.any { ct.startsWith(it) }
148
+ }
149
+
150
+ private fun binaryPlaceholder(contentType: String?): String {
151
+ val ct = contentType ?: "unknown"
152
+ val mediaType = when {
153
+ ct.startsWith("image/") -> "Image"
154
+ ct.startsWith("video/") -> "Video"
155
+ ct.startsWith("audio/") -> "Audio"
156
+ else -> "Binary"
157
+ }
158
+ return "[$mediaType preview not yet supported ($ct)]"
159
+ }
124
160
  }
125
161
 
126
162
  private fun Map<String, String>.toJsObject(): JSObject {
@@ -7,6 +7,7 @@ class LoggerConfigProvider {
7
7
  fun getConfig(plugin: Plugin): JSObject {
8
8
  val config = plugin.bridge.config.getPluginConfiguration("PicaNetworkLogger")
9
9
  val output = JSObject()
10
+ output.put("enabled", config.getBoolean("enabled", true))
10
11
  output.put("maxBodySize", config.getInt("maxBodySize", 131072))
11
12
  output.put("redactHeaders", config.getArray("redactHeaders"))
12
13
  output.put("redactJsonFields", config.getArray("redactJsonFields"))
@@ -14,11 +14,14 @@ import org.json.JSONObject
14
14
  class PicaNetworkLoggerPlugin : Plugin() {
15
15
  private val repository = LogRepository()
16
16
  private val configProvider = LoggerConfigProvider()
17
+ private var enabled = true
17
18
 
18
19
  override fun load() {
19
20
  super.load()
20
- repository.attach(bridge.context)
21
21
  val config = configProvider.getConfig(this)
22
+ enabled = config.optBoolean("enabled", true)
23
+ if (!enabled) return
24
+ repository.attach(bridge.context)
22
25
  LogRepositoryStore.attach(bridge.context, repository, config.getInt("maxBodySize"))
23
26
  val redactHeaders = config.get("redactHeaders")?.let { value ->
24
27
  when (value) {
@@ -54,6 +57,12 @@ class PicaNetworkLoggerPlugin : Plugin() {
54
57
 
55
58
  @PluginMethod
56
59
  fun startRequest(call: PluginCall) {
60
+ if (!enabled) {
61
+ val ret = JSObject()
62
+ ret.put("id", "")
63
+ call.resolve(ret)
64
+ return
65
+ }
57
66
  val method = call.getString("method") ?: "GET"
58
67
  val url = call.getString("url") ?: ""
59
68
  val headers = call.getObject("headers")?.let { obj ->
@@ -69,6 +78,10 @@ class PicaNetworkLoggerPlugin : Plugin() {
69
78
 
70
79
  @PluginMethod
71
80
  fun finishRequest(call: PluginCall) {
81
+ if (!enabled) {
82
+ call.resolve()
83
+ return
84
+ }
72
85
  val id = call.getString("id") ?: return call.reject("Missing id")
73
86
  val status = call.getInt("status")
74
87
  val headers = call.getObject("headers")?.let { obj ->
@@ -82,6 +95,10 @@ class PicaNetworkLoggerPlugin : Plugin() {
82
95
 
83
96
  @PluginMethod
84
97
  fun openInspector(call: PluginCall) {
98
+ if (!enabled) {
99
+ call.resolve()
100
+ return
101
+ }
85
102
  val context = bridge.context
86
103
  val intent = android.content.Intent(context, InspectorActivity::class.java)
87
104
  intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -4,8 +4,8 @@ class InspectorLogger {
4
4
  static let shared = InspectorLogger()
5
5
  private let repository = LogRepository.shared
6
6
  private var maxBodySize: Int = 131072
7
- private var redactHeaders: Set<String> = ["authorization", "cookie"]
8
- private var redactJsonFields: Set<String> = ["password", "token"]
7
+ private var redactHeaders: Set<String> = []
8
+ private var redactJsonFields: Set<String> = []
9
9
  private var notifyEnabled: Bool = true
10
10
  private var requestMethods: [String: String] = [:]
11
11
  private var requestUrls: [String: String] = [:]
@@ -64,9 +64,15 @@ class InspectorLogger {
64
64
  payload["headers"] = redact(headers)
65
65
  }
66
66
  if let body = body {
67
- let truncated = truncate(redactJson(body))
68
- payload["responseBody"] = truncated.value
69
- payload["responseBodyTruncated"] = truncated.truncated
67
+ let contentType = contentType(from: headers)
68
+ if isBinaryContentType(contentType) {
69
+ payload["responseBody"] = binaryPlaceholder(contentType)
70
+ payload["responseBodyTruncated"] = false
71
+ } else {
72
+ let truncated = truncate(redactJson(body))
73
+ payload["responseBody"] = truncated.value
74
+ payload["responseBodyTruncated"] = truncated.truncated
75
+ }
70
76
  }
71
77
  if let error = error {
72
78
  payload["error"] = error
@@ -154,4 +160,37 @@ class InspectorLogger {
154
160
  func setNotify(enabled: Bool) {
155
161
  notifyEnabled = enabled
156
162
  }
163
+
164
+ private func contentType(from headers: [String: Any]?) -> String? {
165
+ guard let headers = headers else { return nil }
166
+ for (key, value) in headers {
167
+ if key.lowercased() == "content-type", let str = value as? String {
168
+ return str.lowercased()
169
+ }
170
+ }
171
+ return nil
172
+ }
173
+
174
+ private func isBinaryContentType(_ contentType: String?) -> Bool {
175
+ guard let ct = contentType else { return false }
176
+ let binaryPrefixes = ["image/", "video/", "audio/", "application/octet-stream",
177
+ "application/pdf", "application/zip", "application/gzip",
178
+ "font/", "application/vnd.", "application/x-protobuf"]
179
+ return binaryPrefixes.contains { ct.hasPrefix($0) }
180
+ }
181
+
182
+ private func binaryPlaceholder(_ contentType: String?) -> String {
183
+ let ct = contentType ?? "unknown"
184
+ let mediaType: String
185
+ if ct.hasPrefix("image/") {
186
+ mediaType = "Image"
187
+ } else if ct.hasPrefix("video/") {
188
+ mediaType = "Video"
189
+ } else if ct.hasPrefix("audio/") {
190
+ mediaType = "Audio"
191
+ } else {
192
+ mediaType = "Binary"
193
+ }
194
+ return "[\(mediaType) preview not yet supported (\(ct))]"
195
+ }
157
196
  }
@@ -14,8 +14,13 @@ public class PicaNetworkLoggerPlugin: CAPPlugin, CAPBridgedPlugin {
14
14
  CAPPluginMethod(name: "openInspector", returnType: CAPPluginReturnPromise)
15
15
  ]
16
16
  private let configProvider = LoggerConfigProvider()
17
+ private var enabled = true
17
18
 
18
19
  @objc func startRequest(_ call: CAPPluginCall) {
20
+ guard enabled else {
21
+ call.resolve(["id": ""])
22
+ return
23
+ }
19
24
  let id = UUID().uuidString
20
25
  let method = call.getString("method") ?? "GET"
21
26
  let url = call.getString("url") ?? ""
@@ -32,6 +37,10 @@ public class PicaNetworkLoggerPlugin: CAPPlugin, CAPBridgedPlugin {
32
37
  }
33
38
 
34
39
  @objc func finishRequest(_ call: CAPPluginCall) {
40
+ guard enabled else {
41
+ call.resolve()
42
+ return
43
+ }
35
44
  guard let id = call.getString("id") else {
36
45
  call.reject("Missing id")
37
46
  return
@@ -55,6 +64,8 @@ public class PicaNetworkLoggerPlugin: CAPPlugin, CAPBridgedPlugin {
55
64
  public override func load() {
56
65
  super.load()
57
66
  let config = configProvider.getConfig(self)
67
+ enabled = config["enabled"] as? Bool ?? true
68
+ guard enabled else { return }
58
69
  if let size = config["maxBodySize"] as? Int {
59
70
  InspectorLogger.shared.setMaxBodySize(size)
60
71
  }
@@ -74,6 +85,10 @@ public class PicaNetworkLoggerPlugin: CAPPlugin, CAPBridgedPlugin {
74
85
  }
75
86
 
76
87
  @objc func openInspector(_ call: CAPPluginCall) {
88
+ guard enabled else {
89
+ call.resolve()
90
+ return
91
+ }
77
92
  #if canImport(UIKit)
78
93
  DispatchQueue.main.async { [weak self] in
79
94
  let inspector = UINavigationController(rootViewController: InspectorViewController())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-pica-network-logger",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Capacitor HTTP network logger with debug-only native capture",
5
5
  "license": "MIT",
6
6
  "repository": {