Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .changeset/true-cases-say.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---

---
105 changes: 90 additions & 15 deletions .github/workflows/sdk-compliance-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,104 @@ name: SDK Compliance Tests

on:
push:
branches: [ main ]
paths:
branches: [main]
paths: &paths
- 'posthog/**'
- 'posthog-android/**'
- 'posthog-server/**'
- 'sdk_compliance_adapter/**'
- 'buildSrc/**'
- 'build.gradle.kts'
- 'settings.gradle.kts'
- 'gradle.properties'
- 'gradle/**'
- 'gradlew*'
- '.github/workflows/sdk-compliance-tests.yml'
pull_request:
branches: [ main ]
paths:
- 'posthog/**'
- 'sdk_compliance_adapter/**'
- '.github/workflows/sdk-compliance-tests.yml'
branches: [main]
paths: *paths
workflow_dispatch:

permissions:
contents: read
packages: read
pull-requests: write

jobs:
test-android-sdk:
uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@6d19abb9c81e2262dacbe340e7dddda9c871c178
with:
adapter-dockerfile: sdk_compliance_adapter/Dockerfile
adapter-context: .
test-harness-version: "0.10.0"
report-name: android-sdk-compliance-report
jvm:
name: ${{ matrix.profile }} / V0 gzip
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
profile: [core, server]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
java-version: '17'
distribution: temurin
- name: Build and test adapter
run: ./gradlew -Pcompliance :sdk_compliance_adapter:test :sdk_compliance_adapter:installDist --no-daemon
- name: Start public SDK profile
env:
SDK_PROFILE: ${{ matrix.profile }}
PORT: '18290'
run: |
mkdir -p report
sdk_compliance_adapter/build/install/sdk_compliance_adapter/bin/sdk_compliance_adapter > report/adapter.log 2>&1 &
echo $! > report/adapter.pid
- name: Run harness
continue-on-error: true
run: bash sdk_compliance_adapter/run-harness.sh 18290 19290
- name: Verify complete inventory
if: always()
run: python3 sdk_compliance_adapter/check-report.py report/compliance.json
- name: Upload profile evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.profile }}-sdk-compliance-report
path: report/
if-no-files-found: error
- name: Stop adapter
if: always()
run: test ! -f report/adapter.pid || kill "$(cat report/adapter.pid)"

android-entry:
name: Android public entry / V0 gzip
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
java-version: '17'
distribution: temurin
- name: Build native adapter APK
# The SDK disables debug variants when CI=true.
run: CI=false ./gradlew -Pcompliance -PcomplianceAndroid :sdk_compliance_adapter:android:assembleDebug --no-daemon
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Run harness on Android emulator
continue-on-error: true
uses: reactivecircus/android-emulator-runner@4c44018e59b437e86cdfc41da381398f93ed8808 # v2
with:
api-level: 35
arch: x86_64
target: google_apis
disable-animations: true
script: bash sdk_compliance_adapter/run-android.sh
- name: Verify complete inventory
if: always()
run: python3 sdk_compliance_adapter/check-report.py report/compliance.json
- name: Upload Android evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: android-entry-sdk-compliance-report
path: report/
if-no-files-found: error
140 changes: 108 additions & 32 deletions sdk_compliance_adapter/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,119 @@
# Contributing

This package contains the PostHog Android SDK compliance adapter used with the PostHog SDK Test Harness.

## Prerequisites

- x86_64 architecture for the full local test flow because of the Java 8 dependency
- Java 8, 11, and 17 for local Gradle builds

On Apple Silicon, Docker will use emulation. It is slower, but it works.

## Building

### Local build

```bash
./gradlew :sdk_compliance_adapter:build
# SDK compliance profiles

These adapters run the repository's SDK source with harness **1.0.0**, contract 1.2.
Each profile selects **30 capture V0 + 17 feature flag tests**, server wire, gzip.
Assertions are advisory; missing, empty, or incomplete reports fail CI.

| Profile | Actual entry and runtime |
| --- | --- |
| `core` | `com.posthog.PostHog.with` on JVM, integrated context and file queue |
| `server` | `com.posthog.server.PostHog.with` on JVM, SDK memory queue |
| Android entry | `PostHogAndroid.with(applicationContext, config)` in an emulator APK |

Android uses native context, networking, preferences and date providers. Its report
is separate from the core integration and Java server reports. CI uses Android 35
x86_64; the APK requires API 26 or newer for the adapter's Java time/HTTP runtime.

## Configuration and observation

Capture forwards identity, properties and an optional parsed `Date` to the public
SDK. The SDK owns event creation, UUIDs, serialization, compression and retry policy.
The before-send hook observes the final UUID produced by a public call (stateful
capture also builds an intermediate event). It does not assign or edit IDs.

A passive, per-initialization HTTP ingress forwards the SDK's request bytes and
end-to-end headers to the mock, and returns its response unchanged. It does not
retry or follow redirects. The SDK retains its default HTTP client. Host and
hop-by-hop framing headers necessarily belong to each proxy connection. The
controller and proxy use Ktor CIO on both JVM and Android.

`/flush` calls the SDK once and waits up to ten seconds for acknowledgments. It
returns `success: false` if any observed event is still unacknowledged, including
terminal drops: there is no public queue-completion API. `/state.pending_events`
therefore means **observed, unacknowledged UUIDs**, not native queue depth. Retries
are observed repeated UUIDs; a retry budget is never used to infer completion.
Reset closes the instance, retires its ingress and clears this test app's storage.
Parallel tests are not supported.

The configured flush threshold defaults to 100. Millisecond intervals default to
500 and round up to whole seconds (minimum one second), the SDK's supported unit.
Java server has no public `maxRetries` setting; that input cannot be forwarded.
Gzip is the only profile: the SDK has no public compression-disable switch here.
V1 and dedicated AI capture are not advertised.

### Flags

Core and Android disable flag preload, and Android additionally disables lifecycle,
screen, deep-link and push autocapture. Remote configuration still uses the SDK's
startup path. These are isolated **reload-per-action** profiles, not certification
of default startup/preload behavior or ordinary cached-getter network behavior.

Person/group properties use public setters. Identity and groups use public
`identify`/`group`, retaining their SDK-owned events and reloads. The adapter awaits
those reload callbacks; otherwise it explicitly reloads and reads the public cached
getter. No controller parses flag responses or manufactures called-events.
Once identified, changing to another user requires adapter reset/init; unsupported
identity transitions are rejected before changing SDK state or waiting for a reload.
Multi-group actions can cause multiple SDK requests. Singleton flag-key scope,
per-call GeoIP overrides and compound person-property assertions are deferred
stateful/server-contract differences, but remain selected and visible in reports.

Java uses public `evaluateFlags` with singleton keys, groups, person/group properties
and the supported GeoIP argument, then reads `snapshot.getFlag`. Local evaluation
is disabled and `featureFlagCacheSize=0`, so this is an explicit remote/no-cache
profile regardless of `force_remote`.

The following Java result assertions remain deferred because their legacy-only
`featureFlags` fixtures do not match the SDK's rich v2 parser. They are **not skipped**:

- `feature_flags.request_lifecycle.mock_response_value_is_returned_to_caller`
- `feature_flags.retry_behavior.retries_flags_on_502`
- `feature_flags.retry_behavior.retries_flags_on_504`
- `feature_flags.side_effect_events.get_feature_flag_captures_feature_flag_called_event`

All capture retries/status cases exercise real transports, including Java's
existing 408/429 retention, retry-budget and Retry-After differences. Its slower
retry cadence can also miss fixed harness wait windows. A passing assertion does
not imply these underlying SDK policies are compliant.

## Local JVM build and run

Use JDK 17 and the repository Gradle wrapper. Adapter projects are opt-in:

```sh
./gradlew -Pcompliance :sdk_compliance_adapter:test :sdk_compliance_adapter:installDist
PORT=18290 SDK_PROFILE=core sdk_compliance_adapter/build/install/sdk_compliance_adapter/bin/sdk_compliance_adapter
# Use SDK_PROFILE=server for Java server.
```

### Docker build
Against that process, run the pinned harness (Linux with Docker):

```bash
docker build -f sdk_compliance_adapter/Dockerfile -t posthog-android-adapter .
```sh
bash sdk_compliance_adapter/run-harness.sh 18290 19290
python3 sdk_compliance_adapter/check-report.py report/compliance.json
```

The Dockerfile uses Gradle toolchain auto-download to fetch the required Java versions.

## Running tests
Alternatively build `sdk_compliance_adapter/Dockerfile`, or run Compose from this
directory with `SDK_PROFILE=core` or `SDK_PROFILE=server`. Compose's private network
ports are not published to the host. Docker Compose does not emulate Android.

Tests run automatically in GitHub Actions on pushes to `main`/`master`, pull requests, and manual workflow dispatches.
## Android emulator

### Local Docker Compose run
With an Android SDK and a running emulator:

```bash
cd sdk_compliance_adapter
docker-compose up --build --abort-on-container-exit
```sh
CI=false ./gradlew -Pcompliance -PcomplianceAndroid :sdk_compliance_adapter:android:assembleDebug
# Linux + Docker; set ANDROID_SERIAL if other devices are attached.
bash sdk_compliance_adapter/run-android.sh
```

This runs:
The script installs the test APK, starts its HTTP listener, and forwards adapter
port 18292 and reverses mock port 19292. For a native harness installation on macOS,
use the same `adb install`, `forward`, `reverse` and `am start` commands in the script,
then run harness 1.0.0 with those URLs and `--sdk-type server --concurrency 1`.
Only use local/mock project keys; no live PostHog project is needed.

- `test-harness` - compliance test runner
- `adapter` - this SDK adapter
- `mock-server` - mock PostHog server
Adapter regression tests cover both public JVM entries, Date/UUID wire fidelity,
real flag parsing/called-events and passive proxy handling of the trailing-slash
flags endpoint. Test fixtures include a genuine rich v2 response independently of
the harness's legacy-only fixtures.
22 changes: 5 additions & 17 deletions sdk_compliance_adapter/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
# Build with the repo's own Gradle wrapper: an image-pinned Gradle skews from
# the wrapper-generated lockfiles on every wrapper bump (the embedded Kotlin
# build-tools version is locked strictly per Gradle release).
FROM --platform=linux/amd64 eclipse-temurin:17-jdk AS builder

FROM eclipse-temurin:17-jdk AS builder
WORKDIR /app

COPY . .
RUN ./gradlew -Pcompliance :sdk_compliance_adapter:installDist --no-daemon

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

FROM eclipse-temurin:11-jre

FROM eclipse-temurin:17-jre
WORKDIR /app

COPY --from=builder /app/sdk_compliance_adapter/build/install/sdk_compliance_adapter /app

ARG SDK_PROFILE=core
ENV SDK_PROFILE=${SDK_PROFILE}
EXPOSE 8080

CMD ["/app/bin/sdk_compliance_adapter"]
38 changes: 38 additions & 0 deletions sdk_compliance_adapter/android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
plugins {
id("com.android.application")
kotlin("android")
}

android {
namespace = "com.posthog.compliance.android"
compileSdk = PosthogBuildConfig.Android.COMPILE_SDK
defaultConfig {
applicationId = "com.posthog.compliance.android"
minSdk = 26
targetSdk = PosthogBuildConfig.Android.TARGET_SDK
versionCode = 1
versionName = "1.0.0"
}
compileOptions {
sourceCompatibility = PosthogBuildConfig.Build.JAVA_VERSION
targetCompatibility = PosthogBuildConfig.Build.JAVA_VERSION
}
packaging.resources.excludes += setOf("META-INF/INDEX.LIST", "META-INF/io.netty.versions.properties")
}

kotlin {
jvmToolchain(PosthogBuildConfig.Build.JDK_VERSION)
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions.postHogConfig(false)
}

dependencies {
implementation(project(":sdk_compliance_adapter:common"))
implementation(project(":posthog-android"))
}

tasks.matching { it.name == "apiCheck" || it.name == "apiDump" }.configureEach {
enabled = false
}
Loading
Loading