Skip to content

Commit 63fcedf

Browse files
committed
fix
1 parent dd9e94b commit 63fcedf

File tree

7 files changed

+162
-48
lines changed

7 files changed

+162
-48
lines changed

posthog/src/main/java/com/posthog/internal/GzipRequestInterceptor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import java.io.IOException
3636
* This interceptor compresses the HTTP request body. Many webservers can't handle this!
3737
* @property config The Config
3838
*/
39-
internal class GzipRequestInterceptor(private val config: PostHogConfig) : Interceptor {
39+
public class GzipRequestInterceptor(private val config: PostHogConfig) : Interceptor {
4040
@Throws(IOException::class)
4141
override fun intercept(chain: Interceptor.Chain): Response {
4242
val originalRequest = chain.request()

sdk_compliance_adapter/Dockerfile

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ WORKDIR /app
44

55
COPY . .
66

7-
RUN gradle :sdk_compliance_adapter:build --no-daemon
7+
RUN echo 'include(":sdk_compliance_adapter")' >> settings.gradle.kts && \
8+
sed -i '/^dependencyResolutionManagement/i plugins {\n id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"\n}' settings.gradle.kts && \
9+
mkdir -p /root/.gradle && \
10+
echo 'org.gradle.java.installations.auto-download=true' > /root/.gradle/gradle.properties && \
11+
gradle :sdk_compliance_adapter:installDist --no-daemon
812

913
FROM eclipse-temurin:11-jre
1014

1115
WORKDIR /app
1216

13-
# Copy the built JAR
14-
COPY --from=builder /app/sdk_compliance_adapter/build/libs/*.jar /app/adapter.jar
17+
COPY --from=builder /app/sdk_compliance_adapter/build/install/sdk_compliance_adapter /app
1518

16-
# Expose port
1719
EXPOSE 8080
1820

19-
# Run the adapter
20-
CMD ["java", "-jar", "/app/adapter.jar"]
21+
CMD ["/app/bin/sdk_compliance_adapter"]

sdk_compliance_adapter/README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# PostHog Android SDK Compliance Adapter
2+
3+
This compliance adapter wraps the PostHog Android SDK and exposes a standardized HTTP API for automated compliance testing using the [PostHog SDK Test Harness](https://github.com/PostHog/posthog-sdk-test-harness).
4+
5+
## Quick Start
6+
7+
### Running Tests in CI (Recommended)
8+
9+
Tests run automatically in GitHub Actions on:
10+
- Push to `main`/`master` branch
11+
- Pull requests
12+
- Manual trigger via `workflow_dispatch`
13+
14+
See `.github/workflows/sdk-compliance-tests.yml`
15+
16+
### Running Tests Locally
17+
18+
**Note:** Requires x86_64 architecture due to Java 8 dependency. On Apple Silicon, Docker will use emulation (slower but works).
19+
20+
```bash
21+
cd sdk_compliance_adapter
22+
docker-compose up --build --abort-on-container-exit
23+
```
24+
25+
## Architecture
26+
27+
- **adapter.kt** - Ktor HTTP server implementing the compliance adapter API
28+
- **TrackingInterceptor** - OkHttp interceptor for monitoring SDK HTTP requests
29+
- **Dockerfile** - Multi-stage Docker build (requires x86_64 for Java 8)
30+
- **docker-compose.yml** - Local test orchestration
31+
32+
## SDK Modifications
33+
34+
To enable request tracking, we added an optional `httpClient` parameter to `PostHogConfig`:
35+
36+
```kotlin
37+
// PostHogConfig.kt
38+
public var httpClient: okhttp3.OkHttpClient? = null
39+
```
40+
41+
This allows the test adapter to inject a custom OkHttpClient with tracking interceptors. **This change is fully backward compatible** - existing code works unchanged.
42+
43+
## Implementation Details
44+
45+
### HTTP Request Tracking
46+
47+
The adapter injects a custom OkHttpClient that:
48+
- Intercepts all `/batch/` requests
49+
- Extracts event UUIDs from request bodies
50+
- Tracks status codes, retry attempts, and event counts
51+
52+
### Event Tracking
53+
54+
Events are tracked via:
55+
1. `beforeSend` hook - Captures UUIDs as events are queued
56+
2. HTTP interceptor - Verifies UUIDs in outgoing requests
57+
58+
### SDK Type
59+
60+
The Android SDK uses **server SDK format**:
61+
- Endpoint: `/batch/`
62+
- Format: `{api_key, batch, sent_at}`
63+
64+
Tests run with `--sdk-type server`.
65+
66+
## Files Created
67+
68+
```
69+
sdk_compliance_adapter/
70+
├── adapter.kt # Main adapter implementation
71+
├── build.gradle.kts # Gradle build configuration
72+
├── Dockerfile # Docker build (x86_64)
73+
├── docker-compose.yml # Local test setup
74+
├── README.md # This file
75+
└── IMPLEMENTATION_NOTES.md # Detailed technical notes
76+
```
77+
78+
## Changes to Core SDK
79+
80+
### posthog/src/main/java/com/posthog/PostHogConfig.kt
81+
- Added `httpClient: OkHttpClient?` parameter
82+
83+
### posthog/src/main/java/com/posthog/internal/PostHogApi.kt
84+
- Modified to use injected `httpClient` if provided
85+
86+
### settings.gradle.kts
87+
- Added `:sdk_compliance_adapter` module
88+
89+
### .github/workflows/sdk-compliance-tests.yml
90+
- GitHub Actions workflow for automated testing
91+
92+
## References
93+
94+
- [Test Harness Repository](https://github.com/PostHog/posthog-sdk-test-harness)
95+
- [Adapter Guide](https://github.com/PostHog/posthog-sdk-test-harness/blob/main/ADAPTER_GUIDE.md)
96+
- [Contract Specification](https://github.com/PostHog/posthog-sdk-test-harness/blob/main/CONTRACT.yaml)
97+
- [Browser SDK Adapter](https://github.com/PostHog/posthog-js/tree/main/packages/browser/sdk_compliance_adapter) (Reference)

sdk_compliance_adapter/build.gradle.kts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
plugins {
2-
kotlin("jvm") version "1.9.22"
2+
kotlin("jvm")
33
application
44
}
55

66
group = "com.posthog.compliance"
77
version = "1.0.0"
88

9-
repositories {
10-
mavenCentral()
11-
}
12-
139
dependencies {
14-
// PostHog Server SDK (simpler for testing)
15-
implementation(project(":posthog-server"))
10+
// PostHog Core SDK
11+
implementation(project(":posthog"))
1612

1713
// Ktor server
1814
val ktorVersion = "2.3.7"
@@ -42,3 +38,8 @@ java {
4238
sourceCompatibility = JavaVersion.VERSION_11
4339
targetCompatibility = JavaVersion.VERSION_11
4440
}
41+
42+
// Disable API validation for test adapter
43+
tasks.matching { it.name == "apiCheck" || it.name == "apiDump" }.configureEach {
44+
enabled = false
45+
}

sdk_compliance_adapter/docker-compose.yml

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,5 @@
1-
version: '3.8'
2-
31
services:
4-
test-harness:
5-
image: posthog-sdk-test-harness:latest
6-
command: ["--sdk-type", "server"]
7-
depends_on:
8-
- adapter
9-
- mock-server
10-
environment:
11-
- ADAPTER_URL=http://adapter:8080
12-
- MOCK_SERVER_URL=http://mock-server:8081
13-
networks:
14-
- test-network
15-
16-
adapter:
2+
sdk-adapter:
173
build:
184
context: ..
195
dockerfile: sdk_compliance_adapter/Dockerfile
@@ -24,13 +10,13 @@ services:
2410
environment:
2511
- JAVA_TOOL_OPTIONS=-Xmx512m
2612

27-
mock-server:
28-
image: posthog-sdk-test-harness:latest
29-
command: ["mock-server"]
30-
ports:
31-
- "8081:8081"
13+
test-harness:
14+
image: posthog-sdk-test-harness:debug
15+
command: ["run", "--adapter-url", "http://sdk-adapter:8080", "--mock-url", "http://test-harness:8081", "--sdk-type", "server", "--debug"]
3216
networks:
3317
- test-network
18+
depends_on:
19+
- sdk-adapter
3420

3521
networks:
3622
test-network:

sdk_compliance_adapter/adapter.kt renamed to sdk_compliance_adapter/src/main/kotlin/com/posthog/compliance/adapter.kt

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.posthog.compliance
22

33
import com.posthog.PostHog
44
import com.posthog.PostHogConfig
5+
import com.posthog.internal.GzipRequestInterceptor
56
import io.ktor.http.*
67
import io.ktor.serialization.gson.*
78
import io.ktor.server.application.*
@@ -13,6 +14,7 @@ import io.ktor.server.response.*
1314
import io.ktor.server.routing.*
1415
import okhttp3.Interceptor
1516
import okhttp3.Response
17+
import com.posthog.internal.PostHogContext
1618
import java.util.*
1719
import java.util.concurrent.locks.ReentrantLock
1820
import kotlin.concurrent.withLock
@@ -87,11 +89,21 @@ data class SuccessResponse(
8789
val success: Boolean
8890
)
8991

92+
// Minimal context for testing (provides $lib and $lib_version)
93+
class TestPostHogContext(private val sdkName: String, private val sdkVersion: String) : PostHogContext {
94+
override fun getStaticContext(): Map<String, Any> = emptyMap()
95+
override fun getDynamicContext(): Map<String, Any> = emptyMap()
96+
override fun getSdkInfo(): Map<String, Any> = mapOf(
97+
"\$lib" to sdkName,
98+
"\$lib_version" to sdkVersion
99+
)
100+
}
101+
90102
// Global state
91103
object AdapterContext {
92104
val state = AdapterState()
93105
val lock = ReentrantLock()
94-
var postHog: PostHog? = null
106+
var postHog: com.posthog.PostHogInterface? = null
95107
val capturedEvents = mutableListOf<String>() // Store UUIDs of captured events
96108
}
97109

@@ -214,21 +226,36 @@ fun main() {
214226
// Close existing instance if any
215227
AdapterContext.postHog?.close()
216228

217-
// Create OkHttpClient with tracking interceptor
229+
// Create config first (needed for GzipRequestInterceptor)
230+
val tempConfig = PostHogConfig(apiKey = req.api_key, host = req.host)
231+
232+
// Create OkHttpClient with tracking interceptor first, then gzip
233+
// Order matters: TrackingInterceptor reads uncompressed body, GzipInterceptor compresses it
218234
val httpClient = okhttp3.OkHttpClient.Builder()
219235
.addInterceptor(TrackingInterceptor())
236+
.addInterceptor(GzipRequestInterceptor(tempConfig))
220237
.build()
221238

222239
// Create new config
240+
val flushIntervalMs = req.flush_interval_ms ?: 100
241+
val flushIntervalSeconds = maxOf(1, flushIntervalMs / 1000) // Min 1 second
242+
223243
val config = PostHogConfig(
224244
apiKey = req.api_key,
225245
host = req.host,
226-
flushAt = req.flush_at ?: 1, // Flush immediately for tests
227-
flushIntervalSeconds = (req.flush_interval_ms ?: 100) / 1000,
246+
flushAt = req.flush_at ?: 1,
247+
flushIntervalSeconds = flushIntervalSeconds,
228248
debug = true,
229-
httpClient = httpClient
249+
httpClient = httpClient,
250+
preloadFeatureFlags = false // Disable to avoid 404 on /flags/
230251
)
231252

253+
// Set storage prefix for file-backed queue
254+
config.storagePrefix = "/tmp/posthog-queue"
255+
256+
// Set minimal context to provide $lib and $lib_version
257+
config.context = TestPostHogContext("posthog-android", "3.28.0")
258+
232259
// Add beforeSend hook to track captured events
233260
config.addBeforeSend { event ->
234261
AdapterContext.lock.withLock {
@@ -254,21 +281,24 @@ fun main() {
254281
println("[ADAPTER] Capturing event: ${req.event} for user: ${req.distinct_id}")
255282

256283
val ph = AdapterContext.lock.withLock {
257-
AdapterContext.postHog ?: run {
258-
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "SDK not initialized"))
259-
return@post
260-
}
284+
AdapterContext.postHog
261285
}
262286

263-
// Set distinct_id
264-
ph.identify(req.distinct_id)
287+
if (ph == null) {
288+
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "SDK not initialized"))
289+
return@post
290+
}
265291

266292
// Remember the count before capturing
267293
val beforeCount = AdapterContext.capturedEvents.size
268294

269-
// Capture event
295+
// Capture event with distinct_id parameter (don't call identify separately)
270296
val properties = req.properties?.toMutableMap() ?: mutableMapOf()
271-
ph.capture(req.event, properties)
297+
ph.capture(
298+
event = req.event,
299+
distinctId = req.distinct_id,
300+
properties = properties
301+
)
272302

273303
// Get the UUID that was just captured (via beforeSend hook)
274304
val uuid = AdapterContext.lock.withLock {

settings.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ rootProject.name = "PostHog"
1818
include(":posthog")
1919
include(":posthog-android")
2020
include(":posthog-server")
21-
include(":sdk_compliance_adapter")
2221

2322
// samples
2423
include(":posthog-samples:posthog-android-sample")

0 commit comments

Comments
 (0)