Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9342398
task: Add `onNetworkRequest` bridge utility
dcalhoun Nov 21, 2025
324fdd8
docs: Direct Claude to format and lint code
dcalhoun Nov 21, 2025
e21e691
task: Add fetch override utility
dcalhoun Nov 21, 2025
88b77fb
refactor: Avoid overriding fetch if network logging is disabled
dcalhoun Nov 21, 2025
d312a38
refactor: Sort functions by usage occurrence
dcalhoun Nov 21, 2025
e0424f3
docs: Direct Claude to order functions by usage occurrence
dcalhoun Nov 21, 2025
8605a61
task: Initialize fetch interceptor
dcalhoun Nov 21, 2025
60c5329
task: Add network logging configuration option
dcalhoun Nov 21, 2025
6a9e340
task: Add iOS network logging delegate methods
dcalhoun Nov 21, 2025
28cbf98
task: Add Android network logging delegate methods
dcalhoun Nov 21, 2025
8c4548c
task: Enable networking logging in the iOS demo app
dcalhoun Nov 21, 2025
b2d8759
task: Enable networking logging in the Android demo app
dcalhoun Nov 21, 2025
7225dc7
fix: Prevent logging logic locking the Android response
dcalhoun Nov 21, 2025
a67bd10
fix: Custom bridge method sends expected Android data
dcalhoun Nov 21, 2025
5b41c7c
fix: Retain values during `toBuilder` conversion
dcalhoun Nov 24, 2025
6f2c642
test: Assert configuration builder properties
dcalhoun Nov 24, 2025
7d84e05
fix: Serialize various request body types
dcalhoun Nov 24, 2025
b487f99
refactor: Organize fetch interceptor tests
dcalhoun Nov 24, 2025
b01c824
refactor: Extract async logging test helper
dcalhoun Nov 24, 2025
dd47a10
refactor: Prefer Vitest utilities over mock call extraction
dcalhoun Nov 24, 2025
157f052
fix: Parse headers from both Header objects and plain objects
dcalhoun Nov 24, 2025
eac9a47
feat: Demo app logs headers
dcalhoun Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ The project follows WordPress coding standards for JavaScript:
- **ESLint**: Uses `@wordpress/eslint-plugin/recommended` configuration
- **Prettier**: Uses `@wordpress/prettier-config` for code formatting

### Function Ordering Convention

Functions in this project are ordered by usage/call order rather than alphabetically:

- **Main/exported functions first**: The primary exported function appears at the top of the file
- **Helper functions follow in call order**: Helper functions are ordered based on when they are first called in the main function
- **Example**: If `mainFunction()` calls `helperA()` then `helperB()`, the file order should be: `mainFunction`, `helperA`, `helperB`

This ordering makes code easier to read top-to-bottom, as you encounter function definitions before needing to understand their implementation details.

### Logging Guidelines

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

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

Always run these commands before committing:
### Pre-Commit Checklist

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

```bash
# Format JavaScript code
make format-js
make fmt-js

# Auto-fix linting errors
make lint-js-fix

# Verify linting passes
make lint-js
```

These commands ensure code quality and prevent lint errors from blocking commits.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ open class EditorConfiguration constructor(
val cookies: Map<String, String>,
val enableAssetCaching: Boolean = false,
val cachedAssetHosts: Set<String> = emptySet(),
val editorAssetsEndpoint: String? = null
val editorAssetsEndpoint: String? = null,
val enableNetworkLogging: Boolean = false
): Parcelable {
companion object {
@JvmStatic
Expand All @@ -48,6 +49,7 @@ open class EditorConfiguration constructor(
private var enableAssetCaching: Boolean = false
private var cachedAssetHosts: Set<String> = emptySet()
private var editorAssetsEndpoint: String? = null
private var enableNetworkLogging: Boolean = false

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

fun build(): EditorConfiguration = EditorConfiguration(
title = title,
Expand All @@ -86,7 +89,8 @@ open class EditorConfiguration constructor(
cookies = cookies,
enableAssetCaching = enableAssetCaching,
cachedAssetHosts = cachedAssetHosts,
editorAssetsEndpoint = editorAssetsEndpoint
editorAssetsEndpoint = editorAssetsEndpoint,
enableNetworkLogging = enableNetworkLogging
)
}

Expand Down Expand Up @@ -114,6 +118,7 @@ open class EditorConfiguration constructor(
if (enableAssetCaching != other.enableAssetCaching) return false
if (cachedAssetHosts != other.cachedAssetHosts) return false
if (editorAssetsEndpoint != other.editorAssetsEndpoint) return false
if (enableNetworkLogging != other.enableNetworkLogging) return false

return true
}
Expand All @@ -137,6 +142,7 @@ open class EditorConfiguration constructor(
result = 31 * result + enableAssetCaching.hashCode()
result = 31 * result + cachedAssetHosts.hashCode()
result = 31 * result + (editorAssetsEndpoint?.hashCode() ?: 0)
result = 31 * result + enableNetworkLogging.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class GutenbergView : WebView {
private var logJsExceptionListener: LogJsExceptionListener? = null
private var autocompleterTriggeredListener: AutocompleterTriggeredListener? = null
private var modalDialogStateListener: ModalDialogStateListener? = null
private var networkRequestListener: NetworkRequestListener? = null

/**
* Stores the contextId from the most recent openMediaLibrary call
Expand Down Expand Up @@ -99,6 +100,10 @@ class GutenbergView : WebView {
modalDialogStateListener = listener
}

fun setNetworkRequestListener(listener: NetworkRequestListener) {
networkRequestListener = listener
}

fun setOnFileChooserRequestedListener(listener: (Intent, Int) -> Unit) {
onFileChooserRequested = listener
}
Expand Down Expand Up @@ -307,6 +312,7 @@ class GutenbergView : WebView {
"editorSettings": $editorSettings,
"locale": "${configuration.locale}",
${if (configuration.editorAssetsEndpoint != null) "\"editorAssetsEndpoint\": \"${configuration.editorAssetsEndpoint}\"," else ""}
"enableNetworkLogging": ${configuration.enableNetworkLogging},
"post": {
"id": ${configuration.postId ?: -1},
"title": "$escapedTitle",
Expand Down Expand Up @@ -403,6 +409,10 @@ class GutenbergView : WebView {
fun onModalDialogClosed(dialogType: String)
}

interface NetworkRequestListener {
fun onNetworkRequest(request: NetworkRequest)
}

fun getTitleAndContent(originalContent: CharSequence, callback: TitleAndContentCallback, completeComposition: Boolean = false) {
if (!isEditorLoaded) {
Log.e("GutenbergView", "You can't change the editor content until it has loaded")
Expand Down Expand Up @@ -609,6 +619,19 @@ class GutenbergView : WebView {
}
}

@JavascriptInterface
fun onNetworkRequest(requestData: String) {
handler.post {
try {
val json = JSONObject(requestData)
val request = NetworkRequest.fromJson(json)
networkRequestListener?.onNetworkRequest(request)
} catch (e: Exception) {
Log.e("GutenbergView", "Error parsing network request: ${e.message}")
}
}
}

fun resetFilePathCallback() {
filePathCallback = null
}
Expand Down Expand Up @@ -690,6 +713,7 @@ class GutenbergView : WebView {
onFileChooserRequested = null
autocompleterTriggeredListener = null
modalDialogStateListener = null
networkRequestListener = null
handler.removeCallbacksAndMessages(null)
this.destroy()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.wordpress.gutenberg

import org.json.JSONObject

data class NetworkRequest(
val url: String,
val method: String,
val requestHeaders: Map<String, String>,
val requestBody: String?,
val status: Int,
val responseHeaders: Map<String, String>,
val responseBody: String?,
val duration: Int
) {
companion object {
fun fromJson(json: JSONObject): NetworkRequest {
return NetworkRequest(
url = json.getString("url"),
method = json.getString("method"),
requestHeaders = jsonObjectToMap(json.getJSONObject("requestHeaders")),
requestBody = json.optString("requestBody").takeIf { it.isNotEmpty() },
status = json.getInt("status"),
responseHeaders = jsonObjectToMap(json.getJSONObject("responseHeaders")),
responseBody = json.optString("responseBody").takeIf { it.isNotEmpty() },
duration = json.getInt("duration")
)
}

private fun jsonObjectToMap(jsonObject: JSONObject): Map<String, String> {
val map = mutableMapOf<String, String>()
val keys = jsonObject.keys()
while (keys.hasNext()) {
val key = keys.next()
map[key] = jsonObject.getString(key)
}
return map
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ package org.wordpress.gutenberg

import org.junit.Test
import org.junit.Assert.*
import org.junit.Before

class EditorConfigurationTest {
private lateinit var editorConfig: EditorConfiguration

@Before
fun setup() {
editorConfig = EditorConfiguration.builder()
@Test
fun `test EditorConfiguration builder sets all properties correctly`() {
val config = EditorConfiguration.builder()
.setTitle("Test Title")
.setContent("Test Content")
.setPostId(123)
Expand All @@ -22,23 +20,34 @@ class EditorConfigurationTest {
.setSiteApiNamespace(arrayOf("wp/v2"))
.setNamespaceExcludedPaths(arrayOf("users"))
.setAuthHeader("Bearer token")
.setEditorSettings("{\"foo\":\"bar\"}")
.setLocale("fr")
.setCookies(mapOf("session" to "abc123"))
.setEnableAssetCaching(true)
.setCachedAssetHosts(setOf("example.com", "cdn.example.com"))
.setEditorAssetsEndpoint("https://example.com/assets")
.setEnableNetworkLogging(true)
.build()
}

@Test
fun `test EditorConfiguration builder creates correct configuration`() {
assertEquals("Test Title", editorConfig.title)
assertEquals("Test Content", editorConfig.content)
assertEquals(123, editorConfig.postId)
assertEquals("post", editorConfig.postType)
assertTrue(editorConfig.themeStyles)
assertTrue(editorConfig.plugins)
assertFalse(editorConfig.hideTitle)
assertEquals("https://example.com", editorConfig.siteURL)
assertEquals("https://example.com/wp-json", editorConfig.siteApiRoot)
assertArrayEquals(arrayOf("wp/v2"), editorConfig.siteApiNamespace)
assertArrayEquals(arrayOf("users"), editorConfig.namespaceExcludedPaths)
assertEquals("Bearer token", editorConfig.authHeader)
assertEquals("Test Title", config.title)
assertEquals("Test Content", config.content)
assertEquals(123, config.postId)
assertEquals("post", config.postType)
assertTrue(config.themeStyles)
assertTrue(config.plugins)
assertFalse(config.hideTitle)
assertEquals("https://example.com", config.siteURL)
assertEquals("https://example.com/wp-json", config.siteApiRoot)
assertArrayEquals(arrayOf("wp/v2"), config.siteApiNamespace)
assertArrayEquals(arrayOf("users"), config.namespaceExcludedPaths)
assertEquals("Bearer token", config.authHeader)
assertEquals("{\"foo\":\"bar\"}", config.editorSettings)
assertEquals("fr", config.locale)
assertEquals(mapOf("session" to "abc123"), config.cookies)
assertTrue(config.enableAssetCaching)
assertEquals(setOf("example.com", "cdn.example.com"), config.cachedAssetHosts)
assertEquals("https://example.com/assets", config.editorAssetsEndpoint)
assertTrue(config.enableNetworkLogging)
}

@Test
Expand Down Expand Up @@ -71,4 +80,4 @@ class EditorConfigurationTest {

assertNotEquals(config1, config2)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,36 @@ fun EditorScreen(
hasRedoState = hasRedo
}
})
setNetworkRequestListener(object : GutenbergView.NetworkRequestListener {
override fun onNetworkRequest(request: org.wordpress.gutenberg.NetworkRequest) {
android.util.Log.d("EditorActivity", "🌐 Network Request: ${request.method} ${request.url}")
android.util.Log.d("EditorActivity", " Status: ${request.status}, Duration: ${request.duration}ms")

// Log request headers
if (request.requestHeaders.isNotEmpty()) {
android.util.Log.d("EditorActivity", " Request Headers:")
request.requestHeaders.toSortedMap().forEach { (key, value) ->
android.util.Log.d("EditorActivity", " $key: $value")
}
}

request.requestBody?.let {
android.util.Log.d("EditorActivity", " Request Body: ${it.take(200)}...")
}

// Log response headers
if (request.responseHeaders.isNotEmpty()) {
android.util.Log.d("EditorActivity", " Response Headers:")
request.responseHeaders.toSortedMap().forEach { (key, value) ->
android.util.Log.d("EditorActivity", " $key: $value")
}
}

request.responseBody?.let {
android.util.Log.d("EditorActivity", " Response Body: ${it.take(200)}...")
}
}
})
start(configuration)
onGutenbergViewCreated(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class MainActivity : ComponentActivity(), AuthenticationManager.AuthenticationCa
.setThemeStyles(false)
.setHideTitle(false)
.setCookies(emptyMap())
.setEnableNetworkLogging(true)

private fun launchEditor(configuration: EditorConfiguration) {
val intent = Intent(this, EditorActivity::class.java)
Expand Down
2 changes: 2 additions & 0 deletions ios/Demo-iOS/Sources/Views/AppRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ struct AppRootView: View {
.setAuthHeader(config.authHeader)
.setNativeInserterEnabled(isNativeInserterEnabled)
.setLogLevel(.debug)
.setEnableNetworkLogging(true)
.build()

self.activeEditorConfiguration = updatedConfiguration
Expand All @@ -102,6 +103,7 @@ struct AppRootView: View {
.setSiteApiRoot("")
.setAuthHeader("")
.setNativeInserterEnabled(isNativeInserterEnabled)
.setEnableNetworkLogging(true)
.build()
}
}
Expand Down
29 changes: 29 additions & 0 deletions ios/Demo-iOS/Sources/Views/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,35 @@ private struct _EditorView: UIViewControllerRepresentable {
func editor(_ viewController: EditorViewController, didCloseModalDialog dialogType: String) {
viewModel.isModalDialogOpen = false
}

func editor(_ viewController: EditorViewController, didLogNetworkRequest request: NetworkRequest) {
print("🌐 Network Request: \(request.method) \(request.url)")
print(" Status: \(request.status), Duration: \(request.duration)ms")

// Log request headers
if !request.requestHeaders.isEmpty {
print(" Request Headers:")
for (key, value) in request.requestHeaders.sorted(by: { $0.key < $1.key }) {
print(" \(key): \(value)")
}
}

if let requestBody = request.requestBody {
print(" Request Body: \(requestBody.prefix(200))...")
}

// Log response headers
if !request.responseHeaders.isEmpty {
print(" Response Headers:")
for (key, value) in request.responseHeaders.sorted(by: { $0.key < $1.key }) {
print(" \(key): \(value)")
}
}

if let responseBody = request.responseBody {
print(" Response Body: \(responseBody.prefix(200))...")
}
}
}
}

Expand Down
Loading