A high-performance event tracking SDK for Kotlin and Java applications.
- 🚀 High Performance: O(1) queue operations with automatic batching
- 🔄 Smart Retry Logic: Exponential backoff with jitter, 4xx vs 5xx handling
- 🔒 Thread-Safe: All operations are thread-safe with mutex-protected flushes
- 💾 Offline Support: Events persisted and sent when connectivity returns
- 🌐 Multi-Platform: Android, Spring Boot, and reactive support
- 📘 Type-Safe: Full Kotlin type safety with Java interoperability
- 🔌 Pluggable Adapters: Customize HTTP, storage, and logging behavior
- ♻️ Re-initializable: Supports dispose/init cycles for lifecycle management
- 🔢 Multi-Instance: Run multiple client instances with different configs
Kotlin DSL (build.gradle.kts)
repositories {
mavenCentral()
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/Tap30/ripple-kotlin")
credentials {
username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME")
password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN")
}
}
}Groovy DSL (build.gradle)
repositories {
mavenCentral()
maven {
name = "GitHubPackages"
url = "https://maven.pkg.github.com/Tap30/ripple-kotlin"
credentials {
username = project.findProperty("gpr.user") ?: System.getenv("USERNAME")
password = project.findProperty("gpr.key") ?: System.getenv("TOKEN")
}
}
}// Core functionality
implementation("io.github.tap30.ripple:core:1.0.0")
// Platform modules (lightweight, no adapters included)
implementation("io.github.tap30.ripple:android-core:1.0.0")
implementation("io.github.tap30.ripple:spring-core:1.0.0")
implementation("io.github.tap30.ripple:reactive-core:1.0.0")Choose only the adapters you need:
Android Adapters
// HTTP with OkHttp
implementation("io.github.tap30.ripple:android-adapters-okhttp:1.0.0")
// → OkHttpAdapter
// Storage with SharedPreferences
implementation("io.github.tap30.ripple:android-adapters-storage-preferences:1.0.0")
// → SharedPreferencesAdapter
// Storage with Room Database
implementation("io.github.tap30.ripple:android-adapters-room:1.0.0")
// → RoomStorageAdapter, RoomStorageAdapterFactory
// Android Logging
implementation("io.github.tap30.ripple:android-adapters-logging:1.0.0")
// → AndroidLogAdapterSpring Adapters
// HTTP with WebClient (WebFlux)
implementation("io.github.tap30.ripple:spring-adapters-webflux:1.0.0")
// → WebClientAdapter
// File System Storage
implementation("io.github.tap30.ripple:spring-adapters-storage-file:1.0.0")
// → FileStorageAdapter
// SLF4J Logging
implementation("io.github.tap30.ripple:spring-adapters-logging:1.0.0")
// → Slf4jLoggerAdapterReactive Adapters
// Project Reactor Support
implementation("io.github.tap30.ripple:reactive-adapters-reactor:1.0.0")
// → ReactorAdapter (coming soon)// Add dependencies
implementation("io.github.tap30.ripple:android-core:1.0.0")
implementation("io.github.tap30.ripple:android-adapters-okhttp:1.0.0")
implementation("io.github.tap30.ripple:android-adapters-room:1.0.0")
// Usage
val config = RippleConfig(
apiKey = "your-api-key",
endpoint = "https://api.example.com/events",
adapters = AdapterConfig(
httpAdapter = OkHttpAdapter(),
storageAdapter = RoomStorageAdapterFactory.create(context),
loggerAdapter = AndroidLogAdapter(LogLevel.INFO)
)
)
val client = AndroidRippleClient(config)
client.init()
// Track events (untyped)
client.track("user_login")
client.track("purchase", mapOf("product_id" to "abc123", "amount" to 29.99))
// Set global metadata
client.setMetadata("user_id", "12345")
// Clean up
client.dispose()Define your events with compile-time validation:
// Define events as sealed class
sealed class AppEvent : RippleEvent {
data class UserLogin(val email: String, val method: String) : AppEvent() {
override val name = "user.login"
override fun toPayload() = mapOf("email" to email, "method" to method)
}
data class Purchase(val orderId: String, val amount: Double) : AppEvent() {
override val name = "purchase"
override fun toPayload() = mapOf("orderId" to orderId, "amount" to amount)
}
}
// Define metadata
data class AppMetadata(
val userId: String? = null,
val version: String? = null
) : RippleMetadata {
override fun toMap() = buildMap {
userId?.let { put("userId", it) }
version?.let { put("version", it) }
}
}
// Usage - compile-time type checking
client.track(AppEvent.UserLogin("user@example.com", "google"))
client.track(AppEvent.Purchase("ORD-123", 99.99))
client.setMetadata(AppMetadata(userId = "user-123", version = "1.0.0"))
// Event-specific metadata
client.track(AppEvent.Purchase("ORD-456", 50.0), AppMetadata(userId = "vip-user"))@Configuration
class RippleConfiguration {
@Bean
fun rippleClient(): SpringRippleClient {
val config = RippleConfig(
apiKey = "your-api-key",
endpoint = "https://api.example.com/events",
flushInterval = 5000L,
maxBatchSize = 20,
adapters = AdapterConfig(
httpAdapter = WebClientAdapter(),
storageAdapter = FileStorageAdapter(),
loggerAdapter = Slf4jLoggerAdapter()
)
)
return SpringRippleClient(config).apply {
init()
setMetadata("service", "user-service")
setMetadata("environment", "production")
}
}
}
@Service
class UserService(private val rippleClient: SpringRippleClient) {
fun createUser(user: User) {
// Business logic...
rippleClient.track("user_created", mapOf(
"user_id" to user.id,
"plan" to user.plan
))
}
}@Configuration
public class RippleConfiguration {
@Bean
public SpringRippleClient rippleClient() {
RippleConfig config = new RippleConfig(
"your-api-key",
"https://api.example.com/events",
"X-API-Key",
5000L,
10,
3,
new AdapterConfig(
new WebClientAdapter(),
new FileStorageAdapter(),
new Slf4jLoggerAdapter(LogLevel.INFO)
)
);
SpringRippleClient client = new SpringRippleClient(config);
client.init();
client.setMetadata("service", "java-service");
return client;
}
}
@Service
public class UserService {
private final SpringRippleClient rippleClient;
public UserService(SpringRippleClient rippleClient) {
this.rippleClient = rippleClient;
}
public void createUser(User user) {
// Business logic...
Map<String, Object> payload = new HashMap<>();
payload.put("user_id", user.getId());
payload.put("plan", user.getPlan());
rippleClient.track("user_created", payload, null);
}
}| Method | Description |
|---|---|
init() |
Initialize the client. Must be called before tracking. Can be called after dispose(). |
track(name, payload?, metadata?) |
Track an event with optional payload and metadata. |
setMetadata(key, value) |
Set global metadata attached to all events. |
getMetadata() |
Get all stored metadata as a shallow copy. |
getSessionId() |
Get the current session ID. |
removeMetadata(key) |
Remove a global metadata key. |
clearMetadata() |
Clear all global metadata. |
flush() |
Flush queued events asynchronously. |
flushSync() |
Flush queued events and wait for completion. |
getQueueSize() |
Get the number of queued events. |
dispose() |
Clean up resources. Persists unsent events. Supports re-initialization. |
| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey |
String | required | API authentication key |
endpoint |
String | required | API endpoint URL |
apiKeyHeader |
String | "X-API-Key" | Header name for API key |
flushInterval |
Long | 5000 | Auto-flush interval in milliseconds |
maxBatchSize |
Int | 10 | Maximum events per batch |
maxRetries |
Int | 3 | Maximum retry attempts |
adapters |
AdapterConfig | required | Platform adapters |
All public methods are thread-safe and can be called from any thread:
track()is non-blocking and returns immediatelyflush()is non-blocking and submits work to background threadflushSync()blocks until completion (use for critical events)- Multiple concurrent calls are handled gracefully
Events are automatically persisted when:
- Network requests fail after all retries
dispose()is called with events in queue
Persisted events are restored on next init() call.
The SDK supports running multiple client instances simultaneously with different configurations:
// Analytics client
val analyticsClient = AndroidRippleClient(context, RippleConfig(
apiKey = "analytics-key",
endpoint = "https://analytics.example.com/events",
adapters = AdapterConfig(...)
))
// Monitoring client
val monitoringClient = AndroidRippleClient(context, RippleConfig(
apiKey = "monitoring-key",
endpoint = "https://monitoring.example.com/events",
adapters = AdapterConfig(...)
))
analyticsClient.init()
monitoringClient.init()
// Track to different endpoints
analyticsClient.track("user_action", mapOf("action" to "click"))
monitoringClient.track("performance", mapOf("latency" to 150))Each instance maintains its own:
- Event queue
- Metadata storage
- Session ID
- Flush scheduler
The SDK handles HTTP errors intelligently:
| Status Code | Behavior |
|---|---|
| 2xx | Success - clear storage |
| 4xx | No retry - persist events |
| 5xx | Retry with exponential backoff |
| Network error | Retry with exponential backoff |
Backoff formula: delay = (1000ms × 2^attempt) + jitter(0-1000ms), max 30 seconds.
- Detailed Documentation - Complete API reference
- GitHub Packages Setup - Authentication guide
- Contributing - Development guidelines
- Changelog - Release history
MIT License - see LICENSE file for details.