From cbb5dede3ef65ec0c6d97eec7b18c6ef3151403a Mon Sep 17 00:00:00 2001 From: Igor Macedo Quintanilha Date: Thu, 22 Jan 2026 12:26:17 -0300 Subject: [PATCH] feat: log HTTP request headers in debug mode In debug mode, the HTTP API now logs all request headers that are being sent before each API call. This helps developers troubleshoot issues by seeing exactly what headers are included in requests (User-Agent, Content-Type, Authorization, etc.). Logging is added for all endpoints: batch, snapshot, flags, remoteConfig, and localEvaluation. --- .../java/com/posthog/internal/PostHogApi.kt | 22 +++ .../com/posthog/internal/PostHogApiTest.kt | 130 ++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt index 7e207a48..9972bea1 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt @@ -63,6 +63,8 @@ public class PostHogApi( config.serializer.serialize(batch, it.bufferedWriter()) } + logRequestHeaders(request) + client.newCall(request).execute().use { val response = logResponse(it) @@ -85,6 +87,8 @@ public class PostHogApi( config.serializer.serialize(events, it.bufferedWriter()) } + logRequestHeaders(request) + client.newCall(request).execute().use { val response = logResponse(it) @@ -141,6 +145,8 @@ public class PostHogApi( config.serializer.serialize(flagsRequest, it.bufferedWriter()) } + logRequestHeaders(request) + client.newCall(request).execute().use { val response = logResponse(it) @@ -177,6 +183,8 @@ public class PostHogApi( .get() .build() + logRequestHeaders(request) + client.newCall(request).execute().use { val response = logResponse(it) @@ -223,6 +231,8 @@ public class PostHogApi( val request = requestBuilder.get().build() + logRequestHeaders(request) + client.newCall(request).execute().use { val response = logResponse(it) @@ -290,4 +300,16 @@ public class PostHogApi( } } } + + private fun logRequestHeaders(request: Request) { + if (config.debug) { + try { + val headers = request.headers + val headerStrings = headers.names().map { name -> "$name: ${headers[name]}" } + config.logger.log("Request headers for ${request.url}: ${headerStrings.joinToString(", ")}") + } catch (e: Throwable) { + // ignore + } + } + } } diff --git a/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt index 0f85fa98..ec8d09c4 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt @@ -20,12 +20,28 @@ import kotlin.test.assertNull import kotlin.test.assertTrue internal class PostHogApiTest { + private class TestLogger : PostHogLogger { + val messages = mutableListOf() + + override fun log(message: String) { + messages.add(message) + } + + override fun isEnabled(): Boolean = true + } + private fun getSut( host: String, proxy: Proxy? = null, + debug: Boolean = false, + logger: PostHogLogger? = null, ): PostHogApi { val config = PostHogConfig(API_KEY, host) config.proxy = proxy + config.debug = debug + if (logger != null) { + config.logger = logger + } return PostHogApi(config) } @@ -310,4 +326,118 @@ internal class PostHogApiTest { } assertEquals(401, exc.statusCode) } + + // Debug Header Logging Tests + + @Test + fun `batch logs request headers in debug mode`() { + val http = mockHttp() + val url = http.url("/") + val logger = TestLogger() + + val sut = getSut(host = url.toString(), debug = true, logger = logger) + + val event = generateEvent() + sut.batch(listOf(event)) + + assertTrue( + logger.messages.any { it.contains("Request headers for") && it.contains("/batch") }, + "Should log request headers for /batch endpoint", + ) + assertTrue( + logger.messages.any { it.contains("User-Agent:") }, + "Should include User-Agent header in log", + ) + } + + @Test + fun `batch does not log headers when debug is disabled`() { + val http = mockHttp() + val url = http.url("/") + val logger = TestLogger() + + val sut = getSut(host = url.toString(), debug = false, logger = logger) + + val event = generateEvent() + sut.batch(listOf(event)) + + assertFalse( + logger.messages.any { it.contains("Request headers for") }, + "Should not log request headers when debug is disabled", + ) + } + + @Test + fun `flags logs request headers in debug mode`() { + val file = File("src/test/resources/json/flags-v1/basic-flags-no-errors.json") + val responseFlagsApi = file.readText() + + val http = + mockHttp( + response = + MockResponse() + .setBody(responseFlagsApi), + ) + val url = http.url("/") + val logger = TestLogger() + + val sut = getSut(host = url.toString(), debug = true, logger = logger) + + sut.flags("distinctId", anonymousId = "anonId", emptyMap()) + + assertTrue( + logger.messages.any { it.contains("Request headers for") && it.contains("/flags") }, + "Should log request headers for /flags endpoint", + ) + } + + @Test + fun `localEvaluation logs request headers including Authorization in debug mode`() { + val http = + mockHttp( + response = + MockResponse() + .setBody(createLocalEvaluationJson()) + .setHeader("ETag", "\"test-etag\""), + ) + val url = http.url("/") + val logger = TestLogger() + + val sut = getSut(host = url.toString(), debug = true, logger = logger) + + sut.localEvaluation("test-personal-key") + + assertTrue( + logger.messages.any { it.contains("Request headers for") && it.contains("/local_evaluation") }, + "Should log request headers for /local_evaluation endpoint", + ) + assertTrue( + logger.messages.any { it.contains("Authorization:") }, + "Should include Authorization header in log", + ) + } + + @Test + fun `remoteConfig logs request headers in debug mode`() { + val file = File("src/test/resources/json/basic-remote-config.json") + val responseApi = file.readText() + + val http = + mockHttp( + response = + MockResponse() + .setBody(responseApi), + ) + val url = http.url("/") + val logger = TestLogger() + + val sut = getSut(host = url.toString(), debug = true, logger = logger) + + sut.remoteConfig() + + assertTrue( + logger.messages.any { it.contains("Request headers for") && it.contains("/array/") }, + "Should log request headers for /array/ (remoteConfig) endpoint", + ) + } }