From 84d3f39456111ac8a739cfe390c03004ac88242e Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 28 Oct 2026 19:14:52 -0400 Subject: [PATCH 1/3] chore(compliance): run the 1.25.0 test harness against the adapter Bump the harness from 1.20.0 (CI) and 0.10.0 (local docker-compose) to 1.25.0, and declare the 1.25 capabilities the SDK supports: generated_payloads, log_capture, property_layers and fail_open. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/sdk-compliance-tests.yml | 2 +- .../IMPLEMENTATION_NOTES.md | 2 +- sdk_compliance_adapter/README.md | 2 +- sdk_compliance_adapter/docker-compose.yml | 2 +- .../posthog/compliance/ComplianceAdapter.kt | 70 ++++++++++++++++++- 5 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sdk-compliance-tests.yml b/.github/workflows/sdk-compliance-tests.yml index 2f4dc4ee1..8693b76e3 100644 --- a/.github/workflows/sdk-compliance-tests.yml +++ b/.github/workflows/sdk-compliance-tests.yml @@ -26,5 +26,5 @@ jobs: with: adapter-dockerfile: sdk_compliance_adapter/Dockerfile adapter-context: . - test-harness-version: "1.20.0" + test-harness-version: "1.25.0" report-name: android-sdk-compliance-report diff --git a/sdk_compliance_adapter/IMPLEMENTATION_NOTES.md b/sdk_compliance_adapter/IMPLEMENTATION_NOTES.md index 723403d76..b98c39d9a 100644 --- a/sdk_compliance_adapter/IMPLEMENTATION_NOTES.md +++ b/sdk_compliance_adapter/IMPLEMENTATION_NOTES.md @@ -76,7 +76,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for local build and compliance test comma ## SDK Type -The Android SDK advertises `capture_v1`, `capture_ai_v1`, `encoding_gzip` and `event_options`: +The Android SDK advertises `capture_v1`, `capture_ai_v1`, `encoding_gzip`, `event_options`, `generated_payloads`, `log_capture`, `property_layers` and `fail_open`: - Endpoints: `POST /i/v1/analytics/events` (`/capture`) and `POST /i/v1/ai/events` (`/capture_ai`) - Format: `{created_at: "...", batch: [{event}, {event}]}` with the project token in an `Authorization: Bearer` header diff --git a/sdk_compliance_adapter/README.md b/sdk_compliance_adapter/README.md index ca843b536..c71675ef4 100644 --- a/sdk_compliance_adapter/README.md +++ b/sdk_compliance_adapter/README.md @@ -41,7 +41,7 @@ Events are tracked via: ### SDK Type -The Android SDK advertises `capture_v1`, `capture_ai_v1`, `encoding_gzip` and `event_options`: +The Android SDK advertises `capture_v1`, `capture_ai_v1`, `encoding_gzip`, `event_options`, `generated_payloads`, `log_capture`, `property_layers` and `fail_open`: - Endpoints: `POST /i/v1/analytics/events` (`/capture`) and `POST /i/v1/ai/events` (`/capture_ai`) - Format: `{created_at, batch}` with the project token in an `Authorization: Bearer` header diff --git a/sdk_compliance_adapter/docker-compose.yml b/sdk_compliance_adapter/docker-compose.yml index 2a9881cbd..855bf531d 100644 --- a/sdk_compliance_adapter/docker-compose.yml +++ b/sdk_compliance_adapter/docker-compose.yml @@ -11,7 +11,7 @@ services: - JAVA_TOOL_OPTIONS=-Xmx512m test-harness: - image: ghcr.io/posthog/sdk-test-harness:0.10.0 + image: ghcr.io/posthog/sdk-test-harness:1.25.0 command: ["run", "--adapter-url", "http://sdk-adapter:8080", "--mock-url", "http://test-harness:8081", "--sdk-type", "server", "--debug"] networks: - test-network diff --git a/sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt b/sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt index ee0eeea02..77b9dabe6 100644 --- a/sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt +++ b/sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt @@ -1,5 +1,6 @@ package com.posthog.compliance +import com.google.gson.Gson import com.google.gson.JsonParser import com.posthog.PersonProfiles import com.posthog.PostHog @@ -8,6 +9,7 @@ import com.posthog.PostHogConfig import com.posthog.internal.GzipRequestInterceptor import com.posthog.internal.PostHogApi import com.posthog.internal.PostHogContext +import com.posthog.internal.PostHogLogger import io.ktor.http.HttpStatusCode import io.ktor.serialization.gson.gson import io.ktor.server.application.call @@ -25,6 +27,7 @@ import okhttp3.Response import java.io.File import java.time.OffsetDateTime import java.time.format.DateTimeParseException +import java.util.Collections import java.util.Date import java.util.UUID import java.util.concurrent.TimeUnit @@ -82,6 +85,12 @@ data class InitRequest( val flush_interval_ms: Int? = null, val max_retries: Int? = null, val enable_compression: Boolean? = null, + val super_properties: Map? = null, + val before_send: BeforeSendSpec? = null, +) + +data class BeforeSendSpec( + val set_properties: Map? = null, ) data class CaptureRequest( @@ -90,6 +99,8 @@ data class CaptureRequest( val properties: Map? = null, val timestamp: String? = null, val options: Map? = null, + val pad_properties_to_bytes: Int? = null, + val marker: String? = null, ) data class CaptureResponse( @@ -127,6 +138,7 @@ data class StateResponse( val total_retries: Int, val last_error: String?, val requests_made: List, + val log_lines: List, ) data class SuccessResponse( @@ -146,6 +158,16 @@ class TestPostHogContext(private val sdkName: String, private val sdkVersion: St ) } +// Records every SDK log line for /state, and still prints it. +class RecordingLogger : PostHogLogger { + override fun log(message: String) { + println(message) + AdapterContext.logLines.add(message) + } + + override fun isEnabled(): Boolean = true +} + // Global state object AdapterContext { val state = AdapterState() @@ -156,6 +178,9 @@ object AdapterContext { var postHog: com.posthog.PostHogInterface? = null var api: PostHogApi? = null val capturedEvents = mutableListOf() // Store UUIDs of captured events + + // Own lock: the SDK logs from its threads, which must not wait on [lock]. + val logLines: MutableList = Collections.synchronizedList(mutableListOf()) } // OkHttp Interceptor to track requests @@ -252,6 +277,20 @@ private fun parseHarnessTimestamp(timestamp: String?): Date? = } } +// Adds a $test_padding string so [properties], as compact UTF-8 JSON, is exactly [targetBytes] long. +private fun padProperties( + properties: Map?, + targetBytes: Int, + marker: String?, +): Map { + val padded = properties.orEmpty().toMutableMap() + padded["\$test_padding"] = "" + val baseBytes = Gson().toJson(padded).toByteArray(Charsets.UTF_8).size + val prefix = marker.orEmpty() + padded["\$test_padding"] = prefix + "x".repeat(maxOf(0, targetBytes - baseBytes - prefix.length)) + return padded +} + // Block until [predicate] holds (signalled as each capture response lands) or [timeoutMs] elapses. private fun awaitBatch( timeoutMs: Long, @@ -282,7 +321,17 @@ fun main() { adapter_version = "1.0.0", // Opt into the capture suites (android posts /i/v1/analytics/events and // /i/v1/ai/events with gzip). - capabilities = listOf("capture_v1", "capture_ai_v1", "encoding_gzip", "event_options"), + capabilities = + listOf( + "capture_v1", + "capture_ai_v1", + "encoding_gzip", + "event_options", + "generated_payloads", + "log_capture", + "property_layers", + "fail_open", + ), ), ) } @@ -303,6 +352,7 @@ fun main() { // Close existing instance if any AdapterContext.postHog?.close() AdapterContext.api = null + AdapterContext.logLines.clear() // close() leaves queue files on disk; wipe so they don't replay into this test. File(QUEUE_STORAGE_PREFIX).deleteRecursively() @@ -321,6 +371,7 @@ fun main() { debug = true, preloadFeatureFlags = false, ) + config.logger = RecordingLogger() // The harness models a server SDK: it never identifies, and expects person // processing on unless an event opts out. config.personProfiles = PersonProfiles.ALWAYS @@ -359,9 +410,17 @@ fun main() { } event } + req.before_send?.set_properties?.let { setProperties -> + config.addBeforeSend { event -> + event.properties?.putAll(setProperties) + event + } + } // Create PostHog instance and direct API helper for per-request flag evaluation. - AdapterContext.postHog = PostHog.with(config) + val postHog = PostHog.with(config) + req.super_properties?.forEach { (key, value) -> postHog.register(key, value) } + AdapterContext.postHog = postHog AdapterContext.api = PostHogApi(config) } @@ -427,11 +486,14 @@ fun main() { return@post } + val properties = + req.pad_properties_to_bytes?.let { padProperties(req.properties, it, req.marker) } + ?: req.properties val uuid = ph.captureAi( event = req.event, distinctId = req.distinct_id, - properties = req.properties, + properties = properties, timestamp = parseHarnessTimestamp(req.timestamp), options = req.options, ) @@ -536,6 +598,7 @@ fun main() { total_retries = AdapterContext.state.totalRetries, last_error = AdapterContext.state.lastError, requests_made = AdapterContext.state.requestsMade.toList(), + log_lines = synchronized(AdapterContext.logLines) { AdapterContext.logLines.toList() }, ) } @@ -560,6 +623,7 @@ fun main() { AdapterContext.state.lastError = null AdapterContext.state.requestsMade.clear() AdapterContext.capturedEvents.clear() + AdapterContext.logLines.clear() } call.respond(SuccessResponse(success = true)) From 5e15556d39038e848b2705908ede5180314c14d8 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 28 Oct 2026 19:22:40 -0400 Subject: [PATCH 2/3] chore(compliance): count the marker's serialized bytes when padding AI properties --- .../kotlin/com/posthog/compliance/ComplianceAdapter.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt b/sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt index 77b9dabe6..885f3e3d1 100644 --- a/sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt +++ b/sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/ComplianceAdapter.kt @@ -284,10 +284,11 @@ private fun padProperties( marker: String?, ): Map { val padded = properties.orEmpty().toMutableMap() - padded["\$test_padding"] = "" - val baseBytes = Gson().toJson(padded).toByteArray(Charsets.UTF_8).size val prefix = marker.orEmpty() - padded["\$test_padding"] = prefix + "x".repeat(maxOf(0, targetBytes - baseBytes - prefix.length)) + padded["\$test_padding"] = prefix + // Measure with the marker in place so its serialized (escaped, UTF-8) size is counted. + val baseBytes = Gson().toJson(padded).toByteArray(Charsets.UTF_8).size + padded["\$test_padding"] = prefix + "x".repeat(maxOf(0, targetBytes - baseBytes)) return padded } From c4ec4ea1f4253da90ed44224f51ef24d8de44a54 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Wed, 28 Oct 2026 21:49:43 -0400 Subject: [PATCH 3/3] chore(compliance): bump the test harness to 1.26.0 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/sdk-compliance-tests.yml | 2 +- sdk_compliance_adapter/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sdk-compliance-tests.yml b/.github/workflows/sdk-compliance-tests.yml index 8693b76e3..5a2f99035 100644 --- a/.github/workflows/sdk-compliance-tests.yml +++ b/.github/workflows/sdk-compliance-tests.yml @@ -26,5 +26,5 @@ jobs: with: adapter-dockerfile: sdk_compliance_adapter/Dockerfile adapter-context: . - test-harness-version: "1.25.0" + test-harness-version: "1.26.0" report-name: android-sdk-compliance-report diff --git a/sdk_compliance_adapter/docker-compose.yml b/sdk_compliance_adapter/docker-compose.yml index 855bf531d..0bb1241ce 100644 --- a/sdk_compliance_adapter/docker-compose.yml +++ b/sdk_compliance_adapter/docker-compose.yml @@ -11,7 +11,7 @@ services: - JAVA_TOOL_OPTIONS=-Xmx512m test-harness: - image: ghcr.io/posthog/sdk-test-harness:1.25.0 + image: ghcr.io/posthog/sdk-test-harness:1.26.0 command: ["run", "--adapter-url", "http://sdk-adapter:8080", "--mock-url", "http://test-harness:8081", "--sdk-type", "server", "--debug"] networks: - test-network