Skip to content

Commit b961a9a

Browse files
authored
task: Log network activity (#238)
* task: Add `onNetworkRequest` bridge utility * docs: Direct Claude to format and lint code * task: Add fetch override utility * refactor: Avoid overriding fetch if network logging is disabled * refactor: Sort functions by usage occurrence * docs: Direct Claude to order functions by usage occurrence Make reading files easier, encountering high-level logic first before the implementation details of helper utilities. * task: Initialize fetch interceptor * task: Add network logging configuration option * task: Add iOS network logging delegate methods * task: Add Android network logging delegate methods * task: Enable networking logging in the iOS demo app * task: Enable networking logging in the Android demo app * fix: Prevent logging logic locking the Android response All requests were failing due to the blocking processing of the body. * fix: Custom bridge method sends expected Android data The data structure for networking logging is incompatible with `dispatchToBridge`. * fix: Retain values during `toBuilder` conversion The absence of these keys resulted in resetting the value to its default when converting the configuration to a builder. * test: Assert configuration builder properties * fix: Serialize various request body types Avoid logging `[Object object]` string values. * refactor: Organize fetch interceptor tests * refactor: Extract async logging test helper * refactor: Prefer Vitest utilities over mock call extraction Improve assertion explicitness. * fix: Parse headers from both Header objects and plain objects * feat: Demo app logs headers * refactor: Rename `NetworkRequest` to `RecordedNetworkRequest` Further clarify both request and response data is captured. * task: Report status text
1 parent 4d733c3 commit b961a9a

File tree

18 files changed

+1223
-32
lines changed

18 files changed

+1223
-32
lines changed

CLAUDE.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,16 @@ The project follows WordPress coding standards for JavaScript:
173173
- **ESLint**: Uses `@wordpress/eslint-plugin/recommended` configuration
174174
- **Prettier**: Uses `@wordpress/prettier-config` for code formatting
175175

176+
### Function Ordering Convention
177+
178+
Functions in this project are ordered by usage/call order rather than alphabetically:
179+
180+
- **Main/exported functions first**: The primary exported function appears at the top of the file
181+
- **Helper functions follow in call order**: Helper functions are ordered based on when they are first called in the main function
182+
- **Example**: If `mainFunction()` calls `helperA()` then `helperB()`, the file order should be: `mainFunction`, `helperA`, `helperB`
183+
184+
This ordering makes code easier to read top-to-bottom, as you encounter function definitions before needing to understand their implementation details.
185+
176186
### Logging Guidelines
177187

178188
The project uses a custom logger utility (`src/utils/logger.js`) instead of direct `console` methods:
@@ -186,12 +196,19 @@ The project uses a custom logger utility (`src/utils/logger.js`) instead of dire
186196

187197
Note: Console logs should be used sparingly. For verbose or development-specific logging, prefer the `debug()` function which can be controlled via log levels.
188198

189-
Always run these commands before committing:
199+
### Pre-Commit Checklist
190200

191-
```bash
192-
# Lint JavaScript code
193-
make lint-js
201+
**IMPORTANT**: Always run these commands after making code changes and before presenting work for review/commit:
194202

203+
```bash
195204
# Format JavaScript code
196205
make format
206+
207+
# Auto-fix linting errors
208+
make lint-js-fix
209+
210+
# Verify linting passes
211+
make lint-js
197212
```
213+
214+
These commands ensure code quality and prevent lint errors from blocking commits.

android/Gutenberg/src/main/java/org/wordpress/gutenberg/EditorConfiguration.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ open class EditorConfiguration constructor(
2222
val cookies: Map<String, String>,
2323
val enableAssetCaching: Boolean = false,
2424
val cachedAssetHosts: Set<String> = emptySet(),
25-
val editorAssetsEndpoint: String? = null
25+
val editorAssetsEndpoint: String? = null,
26+
val enableNetworkLogging: Boolean = false
2627
): Parcelable {
2728
companion object {
2829
@JvmStatic
@@ -48,6 +49,7 @@ open class EditorConfiguration constructor(
4849
private var enableAssetCaching: Boolean = false
4950
private var cachedAssetHosts: Set<String> = emptySet()
5051
private var editorAssetsEndpoint: String? = null
52+
private var enableNetworkLogging: Boolean = false
5153

5254
fun setTitle(title: String) = apply { this.title = title }
5355
fun setContent(content: String) = apply { this.content = content }
@@ -67,6 +69,7 @@ open class EditorConfiguration constructor(
6769
fun setEnableAssetCaching(enableAssetCaching: Boolean) = apply { this.enableAssetCaching = enableAssetCaching }
6870
fun setCachedAssetHosts(cachedAssetHosts: Set<String>) = apply { this.cachedAssetHosts = cachedAssetHosts }
6971
fun setEditorAssetsEndpoint(editorAssetsEndpoint: String?) = apply { this.editorAssetsEndpoint = editorAssetsEndpoint }
72+
fun setEnableNetworkLogging(enableNetworkLogging: Boolean) = apply { this.enableNetworkLogging = enableNetworkLogging }
7073

7174
fun build(): EditorConfiguration = EditorConfiguration(
7275
title = title,
@@ -86,7 +89,8 @@ open class EditorConfiguration constructor(
8689
cookies = cookies,
8790
enableAssetCaching = enableAssetCaching,
8891
cachedAssetHosts = cachedAssetHosts,
89-
editorAssetsEndpoint = editorAssetsEndpoint
92+
editorAssetsEndpoint = editorAssetsEndpoint,
93+
enableNetworkLogging = enableNetworkLogging
9094
)
9195
}
9296

@@ -114,6 +118,7 @@ open class EditorConfiguration constructor(
114118
if (enableAssetCaching != other.enableAssetCaching) return false
115119
if (cachedAssetHosts != other.cachedAssetHosts) return false
116120
if (editorAssetsEndpoint != other.editorAssetsEndpoint) return false
121+
if (enableNetworkLogging != other.enableNetworkLogging) return false
117122

118123
return true
119124
}
@@ -137,6 +142,7 @@ open class EditorConfiguration constructor(
137142
result = 31 * result + enableAssetCaching.hashCode()
138143
result = 31 * result + cachedAssetHosts.hashCode()
139144
result = 31 * result + (editorAssetsEndpoint?.hashCode() ?: 0)
145+
result = 31 * result + enableNetworkLogging.hashCode()
140146
return result
141147
}
142148
}

android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergView.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class GutenbergView : WebView {
5555
private var logJsExceptionListener: LogJsExceptionListener? = null
5656
private var autocompleterTriggeredListener: AutocompleterTriggeredListener? = null
5757
private var modalDialogStateListener: ModalDialogStateListener? = null
58+
private var networkRequestListener: NetworkRequestListener? = null
5859

5960
/**
6061
* Stores the contextId from the most recent openMediaLibrary call
@@ -99,6 +100,10 @@ class GutenbergView : WebView {
99100
modalDialogStateListener = listener
100101
}
101102

103+
fun setNetworkRequestListener(listener: NetworkRequestListener) {
104+
networkRequestListener = listener
105+
}
106+
102107
fun setOnFileChooserRequestedListener(listener: (Intent, Int) -> Unit) {
103108
onFileChooserRequested = listener
104109
}
@@ -307,6 +312,7 @@ class GutenbergView : WebView {
307312
"editorSettings": $editorSettings,
308313
"locale": "${configuration.locale}",
309314
${if (configuration.editorAssetsEndpoint != null) "\"editorAssetsEndpoint\": \"${configuration.editorAssetsEndpoint}\"," else ""}
315+
"enableNetworkLogging": ${configuration.enableNetworkLogging},
310316
"post": {
311317
"id": ${configuration.postId ?: -1},
312318
"title": "$escapedTitle",
@@ -403,6 +409,10 @@ class GutenbergView : WebView {
403409
fun onModalDialogClosed(dialogType: String)
404410
}
405411

412+
interface NetworkRequestListener {
413+
fun onNetworkRequest(request: RecordedNetworkRequest)
414+
}
415+
406416
fun getTitleAndContent(originalContent: CharSequence, callback: TitleAndContentCallback, completeComposition: Boolean = false) {
407417
if (!isEditorLoaded) {
408418
Log.e("GutenbergView", "You can't change the editor content until it has loaded")
@@ -609,6 +619,19 @@ class GutenbergView : WebView {
609619
}
610620
}
611621

622+
@JavascriptInterface
623+
fun onNetworkRequest(requestData: String) {
624+
handler.post {
625+
try {
626+
val json = JSONObject(requestData)
627+
val request = RecordedNetworkRequest.fromJson(json)
628+
networkRequestListener?.onNetworkRequest(request)
629+
} catch (e: Exception) {
630+
Log.e("GutenbergView", "Error parsing network request: ${e.message}")
631+
}
632+
}
633+
}
634+
612635
fun resetFilePathCallback() {
613636
filePathCallback = null
614637
}
@@ -690,6 +713,7 @@ class GutenbergView : WebView {
690713
onFileChooserRequested = null
691714
autocompleterTriggeredListener = null
692715
modalDialogStateListener = null
716+
networkRequestListener = null
693717
handler.removeCallbacksAndMessages(null)
694718
this.destroy()
695719
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.wordpress.gutenberg
2+
3+
import org.json.JSONObject
4+
5+
data class RecordedNetworkRequest(
6+
val url: String,
7+
val method: String,
8+
val requestHeaders: Map<String, String>,
9+
val requestBody: String?,
10+
val status: Int,
11+
val statusText: String,
12+
val responseHeaders: Map<String, String>,
13+
val responseBody: String?,
14+
val duration: Int
15+
) {
16+
companion object {
17+
fun fromJson(json: JSONObject): RecordedNetworkRequest {
18+
return RecordedNetworkRequest(
19+
url = json.getString("url"),
20+
method = json.getString("method"),
21+
requestHeaders = jsonObjectToMap(json.getJSONObject("requestHeaders")),
22+
requestBody = json.optString("requestBody").takeIf { it.isNotEmpty() },
23+
status = json.getInt("status"),
24+
statusText = json.optString("statusText", ""),
25+
responseHeaders = jsonObjectToMap(json.getJSONObject("responseHeaders")),
26+
responseBody = json.optString("responseBody").takeIf { it.isNotEmpty() },
27+
duration = json.getInt("duration")
28+
)
29+
}
30+
31+
private fun jsonObjectToMap(jsonObject: JSONObject): Map<String, String> {
32+
val map = mutableMapOf<String, String>()
33+
val keys = jsonObject.keys()
34+
while (keys.hasNext()) {
35+
val key = keys.next()
36+
map[key] = jsonObject.getString(key)
37+
}
38+
return map
39+
}
40+
}
41+
}

android/Gutenberg/src/test/java/org/wordpress/gutenberg/EditorConfigurationTest.kt

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ package org.wordpress.gutenberg
22

33
import org.junit.Test
44
import org.junit.Assert.*
5-
import org.junit.Before
65

76
class EditorConfigurationTest {
8-
private lateinit var editorConfig: EditorConfiguration
97

10-
@Before
11-
fun setup() {
12-
editorConfig = EditorConfiguration.builder()
8+
@Test
9+
fun `test EditorConfiguration builder sets all properties correctly`() {
10+
val config = EditorConfiguration.builder()
1311
.setTitle("Test Title")
1412
.setContent("Test Content")
1513
.setPostId(123)
@@ -22,23 +20,34 @@ class EditorConfigurationTest {
2220
.setSiteApiNamespace(arrayOf("wp/v2"))
2321
.setNamespaceExcludedPaths(arrayOf("users"))
2422
.setAuthHeader("Bearer token")
23+
.setEditorSettings("{\"foo\":\"bar\"}")
24+
.setLocale("fr")
25+
.setCookies(mapOf("session" to "abc123"))
26+
.setEnableAssetCaching(true)
27+
.setCachedAssetHosts(setOf("example.com", "cdn.example.com"))
28+
.setEditorAssetsEndpoint("https://example.com/assets")
29+
.setEnableNetworkLogging(true)
2530
.build()
26-
}
2731

28-
@Test
29-
fun `test EditorConfiguration builder creates correct configuration`() {
30-
assertEquals("Test Title", editorConfig.title)
31-
assertEquals("Test Content", editorConfig.content)
32-
assertEquals(123, editorConfig.postId)
33-
assertEquals("post", editorConfig.postType)
34-
assertTrue(editorConfig.themeStyles)
35-
assertTrue(editorConfig.plugins)
36-
assertFalse(editorConfig.hideTitle)
37-
assertEquals("https://example.com", editorConfig.siteURL)
38-
assertEquals("https://example.com/wp-json", editorConfig.siteApiRoot)
39-
assertArrayEquals(arrayOf("wp/v2"), editorConfig.siteApiNamespace)
40-
assertArrayEquals(arrayOf("users"), editorConfig.namespaceExcludedPaths)
41-
assertEquals("Bearer token", editorConfig.authHeader)
32+
assertEquals("Test Title", config.title)
33+
assertEquals("Test Content", config.content)
34+
assertEquals(123, config.postId)
35+
assertEquals("post", config.postType)
36+
assertTrue(config.themeStyles)
37+
assertTrue(config.plugins)
38+
assertFalse(config.hideTitle)
39+
assertEquals("https://example.com", config.siteURL)
40+
assertEquals("https://example.com/wp-json", config.siteApiRoot)
41+
assertArrayEquals(arrayOf("wp/v2"), config.siteApiNamespace)
42+
assertArrayEquals(arrayOf("users"), config.namespaceExcludedPaths)
43+
assertEquals("Bearer token", config.authHeader)
44+
assertEquals("{\"foo\":\"bar\"}", config.editorSettings)
45+
assertEquals("fr", config.locale)
46+
assertEquals(mapOf("session" to "abc123"), config.cookies)
47+
assertTrue(config.enableAssetCaching)
48+
assertEquals(setOf("example.com", "cdn.example.com"), config.cachedAssetHosts)
49+
assertEquals("https://example.com/assets", config.editorAssetsEndpoint)
50+
assertTrue(config.enableNetworkLogging)
4251
}
4352

4453
@Test
@@ -71,4 +80,4 @@ class EditorConfigurationTest {
7180

7281
assertNotEquals(config1, config2)
7382
}
74-
}
83+
}

android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,36 @@ fun EditorScreen(
225225
hasRedoState = hasRedo
226226
}
227227
})
228+
setNetworkRequestListener(object : GutenbergView.NetworkRequestListener {
229+
override fun onNetworkRequest(request: org.wordpress.gutenberg.RecordedNetworkRequest) {
230+
android.util.Log.d("EditorActivity", "🌐 Network Request: ${request.method} ${request.url}")
231+
android.util.Log.d("EditorActivity", " Status: ${request.status} ${request.statusText}, Duration: ${request.duration}ms")
232+
233+
// Log request headers
234+
if (request.requestHeaders.isNotEmpty()) {
235+
android.util.Log.d("EditorActivity", " Request Headers:")
236+
request.requestHeaders.toSortedMap().forEach { (key, value) ->
237+
android.util.Log.d("EditorActivity", " $key: $value")
238+
}
239+
}
240+
241+
request.requestBody?.let {
242+
android.util.Log.d("EditorActivity", " Request Body: ${it.take(200)}...")
243+
}
244+
245+
// Log response headers
246+
if (request.responseHeaders.isNotEmpty()) {
247+
android.util.Log.d("EditorActivity", " Response Headers:")
248+
request.responseHeaders.toSortedMap().forEach { (key, value) ->
249+
android.util.Log.d("EditorActivity", " $key: $value")
250+
}
251+
}
252+
253+
request.responseBody?.let {
254+
android.util.Log.d("EditorActivity", " Response Body: ${it.take(200)}...")
255+
}
256+
}
257+
})
228258
start(configuration)
229259
onGutenbergViewCreated(this)
230260
}

android/app/src/main/java/com/example/gutenbergkit/MainActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ class MainActivity : ComponentActivity(), AuthenticationManager.AuthenticationCa
160160
.setThemeStyles(false)
161161
.setHideTitle(false)
162162
.setCookies(emptyMap())
163+
.setEnableNetworkLogging(true)
163164

164165
private fun launchEditor(configuration: EditorConfiguration) {
165166
val intent = Intent(this, EditorActivity::class.java)

ios/Demo-iOS/Sources/Views/AppRootView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ struct AppRootView: View {
8080
.setAuthHeader(config.authHeader)
8181
.setNativeInserterEnabled(isNativeInserterEnabled)
8282
.setLogLevel(.debug)
83+
.setEnableNetworkLogging(true)
8384
.build()
8485

8586
self.activeEditorConfiguration = updatedConfiguration
@@ -102,6 +103,7 @@ struct AppRootView: View {
102103
.setSiteApiRoot("")
103104
.setAuthHeader("")
104105
.setNativeInserterEnabled(isNativeInserterEnabled)
106+
.setEnableNetworkLogging(true)
105107
.build()
106108
}
107109
}

ios/Demo-iOS/Sources/Views/EditorView.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,35 @@ private struct _EditorView: UIViewControllerRepresentable {
182182
func editor(_ viewController: EditorViewController, didCloseModalDialog dialogType: String) {
183183
viewModel.isModalDialogOpen = false
184184
}
185+
186+
func editor(_ viewController: EditorViewController, didLogNetworkRequest request: RecordedNetworkRequest) {
187+
print("🌐 Network Request: \(request.method) \(request.url)")
188+
print(" Status: \(request.status) \(request.statusText), Duration: \(request.duration)ms")
189+
190+
// Log request headers
191+
if !request.requestHeaders.isEmpty {
192+
print(" Request Headers:")
193+
for (key, value) in request.requestHeaders.sorted(by: { $0.key < $1.key }) {
194+
print(" \(key): \(value)")
195+
}
196+
}
197+
198+
if let requestBody = request.requestBody {
199+
print(" Request Body: \(requestBody.prefix(200))...")
200+
}
201+
202+
// Log response headers
203+
if !request.responseHeaders.isEmpty {
204+
print(" Response Headers:")
205+
for (key, value) in request.responseHeaders.sorted(by: { $0.key < $1.key }) {
206+
print(" \(key): \(value)")
207+
}
208+
}
209+
210+
if let responseBody = request.responseBody {
211+
print(" Response Body: \(responseBody.prefix(200))...")
212+
}
213+
}
185214
}
186215
}
187216

0 commit comments

Comments
 (0)