Important
This repository is being archived and receives no further releases. Android and
iOS now share the reviewed delivery core in
Pulsepond/mobile-sdk. Android
applications should use dev.pulsepond:pulsepond; its public API and durable
delivery model intentionally differ from this pre-release SDK.
dev.pulsepond:android-sdk sends explicit, privacy-conscious product events
from Android applications to a self-hosted Pulsepond Worker.
Version 0.1 supports Android API 23 and newer. It does not include automatic
capture, advertising identifiers, device fingerprinting, user profiles, or a
persistent event queue.
Maven Central publication is not enabled until the dev.pulsepond namespace is
approved. To test an unreleased checkout locally:
./gradlew publishToMavenLocaldependencies {
implementation("dev.pulsepond:android-sdk:0.1.0")
}Stable GitHub Releases also contain the reviewed AAR, source JAR, and POM.
Create one client for a Pulsepond Android source. The default identity exists only in memory and rotates when the process restarts:
import dev.pulsepond.android.Pulsepond
import dev.pulsepond.android.PulsepondConfig
val pulsepond = Pulsepond.create(
PulsepondConfig(
endpoint = "https://events.example.com/v1/batch",
writeKey = "ppw_v1_...",
environment = "production",
appVersion = "1.4.0",
release = "android@1.4.0",
),
)
val eventId = pulsepond.track(
"view_work",
mapOf("work_id" to "work_123"),
)track() returns the event UUIDv7 or null when the bounded in-memory queue
is full. Invalid event names or properties throw PulsepondValidationException
before anything is enqueued.
Call flush() when the application needs an immediate delivery attempt:
lifecycleScope.launch {
pulsepond.flush()
}Call shutdown() from an application-owned teardown path. It makes one final
bounded delivery attempt and permanently closes the client. The SDK deliberately
does not register an Activity lifecycle observer or WorkManager job.
Persistent random installation and session IDs require an explicit application context and application-owned namespace:
import dev.pulsepond.android.PulsepondIdentityPersistence
val pulsepond = Pulsepond.create(
applicationContext,
PulsepondConfig(
endpoint = "https://events.example.com/v1/batch",
writeKey = "ppw_v1_...",
environment = "production",
persistence = PulsepondIdentityPersistence.DeviceStorage(
namespace = "museum_android",
),
),
)The Context overload is a suspending factory. Call it from an application-owned
coroutine. It reads the small identity file on Dispatchers.IO; subsequent
track() calls update memory synchronously and coalesce persistence through one
background writer. shutdown() drains the last identity update before
returning.
Only random installation/session UUIDs and the last session activity time are
stored in the application's noBackupFilesDir, which Android excludes from
Auto Backup. Pending event payloads are never persisted. A session rotates
after 30 minutes of inactivity; reset() discards unsent events and rotates
both IDs. If storage becomes unavailable, the client falls back to memory and
emits one redacted STORAGE_UNAVAILABLE diagnostic.
Persistent random identifiers and the word "anonymous" are not a compliance guarantee. The application owner remains responsible for disclosure, consent, event design, and regional requirements.
The SDK sends the closed Pulsepond v1 envelope and generates these fields when
track() is called:
- lowercase UUIDv7
event_id schema_version: 1- UTC
occurred_at platform: "android"- configured application and environment fields
- random installation and session UUIDs
- a defensive serialized copy of the explicit properties
Event and property names are ASCII slugs. Properties are flat and limited to
32 values. Values can only be null, booleans, JavaScript-safe integer types,
or trimmed printable ASCII strings up to 256 characters. The Worker remains
authoritative for event/property allowlists and optional PII heuristics.
- Requests go only to the configured exact
/v1/batchURL. - The SDK explicitly sets only
AuthorizationandContent-Typeheaders. - Batches are bounded by event count and 60,000 serialized UTF-8 bytes.
202 Acceptedmeans the Worker accepted the batch into its Queue; it does not promise immediate D1 visibility.- Network failures, timeouts,
408,429, and5xxreceive five bounded retries with full jitter.Retry-Afteris honored up to 30 seconds. - A multi-event
413response is split without changing event IDs. A single-event413is terminal. - Unsent events older than 23 hours are dropped before batching by default.
Delivery is asynchronous and best-effort. Events can be dropped by explicit queue, age, retry, or lifecycle bounds, and ambiguous network failures can produce duplicates. Do not depend on strict ordering or exactly-once delivery.
Use a dedicated Android Source with origin_mode: "forbidden"; normal Android
requests do not carry a browser Origin. The source write key is publishable
and grants ingestion only. It must never grant analytics reads or administrative
access. Event/property allowlists, rate limits, rotation, and revocation remain
required server-side controls.
The SDK does not read or send URLs, screen names, application headers or Cookies, Android advertising ID, Android ID, device model, locale, IP address as an event property, user identity, search text, feedback bodies, or request bodies. The Android networking stack still supplies normal HTTP metadata to the collector; the collector must not copy it into analytics events.
Diagnostics contain only a stable code, retryability, an optional HTTP status, and a dropped-event count. They never contain write keys, event bodies, names, or property values.
| Option | Default | Notes |
|---|---|---|
endpoint |
required | HTTPS URL with the exact /v1/batch path; HTTP is allowed only on localhost |
writeKey |
required | Publishable ppw_v1_... source credential |
environment |
required | ASCII slug, up to 32 characters |
appVersion |
omitted | Trimmed printable ASCII, up to 64 characters |
release |
omitted | Trimmed printable ASCII, up to 128 characters |
persistence |
Memory |
DeviceStorage requires the Context factory and an explicit namespace |
batchSize |
20 |
Between 1 and the protocol maximum of 100 |
flushIntervalMs |
5000 |
0 disables timed flushes |
maxQueueSize |
1000 |
In-memory event-count bound |
eventTtlMs |
23 hours | Align with the Source maximum event age |
onDiagnostic |
omitted | Receives redacted lifecycle and delivery status |
Requirements:
- JDK 17
- Android SDK Platform 37 and Build Tools 36
./gradlew check lint publishToMavenLocalThe quality gate runs protocol, queue, retry, identity, real JVM HTTP, lint,
AAR, source JAR, and POM checks. Pull requests and pushes to main run the same
gate with a read-only GitHub token.
Create a stable GitHub Release whose tag matches v<pulsepond module version>.
The release workflow reruns the full quality gate and uploads the AAR, source
JAR, and POM. Maven Central publication will be added only after namespace and
signing credentials are configured; release code does not contain placeholder
or long-lived secrets.