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", + ) + } }