
Why This Matters
In high-traffic or performance-sensitive applications, missing event logs can create gaps in data, impacting business insights and user experience improvements. This solution ensures that even if rate limits are hit, all events are eventually logged, supporting accurate analytics and decision-making.
Key Challenges in Event Logging
- Rate Limits: Instana restricts the number of logs that can be sent within specific time frames, making it challenging to capture all events in real-time.
- Reliability: Apps may need to retry logging events that exceed rate limits.
- Persistence: Ensuring events are stored and retried even if the app is backgrounded or killed.
Solution Overview
We’ll use Kotlin delegation and WorkManager to:
- Log events reliably with Instana, adhering to rate limits.
- Queue and retry events if they exceed rate limits.
- Persist events so they are not lost if the app is closed.
Objectives
- Reliable logging of events under rate limits.
- Persistent queuing of events to prevent data loss.
- Modular, reusable code using Kotlin delegation.
Step 1: Define an Analytics Event Model
We’ll start by defining an **AnalyticsEvent** data class to keep track of each log's details and timestamp. This will store the properties and timestamp for accuracy.
data class AnalyticsEvent(
val name: String,
val properties: Map<String, String>? = null,
val timestamp: Long = System.currentTimeMillis()
)Step 2: Create the Analytics Handler Interface
Using Kotlin’s delegation, we’ll offload the logging logic to a RateLimitedLogger class. First, define an AnalyticsHandler interface with methods to log events and screen views.
interface AnalyticsHandler {
fun logEvent(event: AnalyticsEvent)
fun logScreenView(screenName: String)
}Step 3: Build a RateLimitedLogger with Persistence
The RateLimitedLogger the class will manage rate limiting and store events that exceed Instana's limit. It queues these events and saves them using SharedPreferences (or Room, if preferred) for retry when limits reset.
RateLimitedLogger Implementation
Here’s the implementation with comments breaking down each section:
class RateLimitedLogger(context: Context) : AnalyticsHandler {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences("analytics_events", Context.MODE_PRIVATE)
private val gson = Gson()
private val rateLimiter = RateLimiter(maxPerTenMinutes = 128, maxPerTenSeconds = 32)
private val eventQueue = loadEventsFromPreferences()
private val workManager = WorkManager.getInstance(context)
// Log an event, checking if it can be sent immediately or queued for later
override fun logEvent(event: AnalyticsEvent) {
if (rateLimiter.canLogEvent()) {
sendToInstana(event)
} else {
eventQueue.add(event)
saveEventsToPreferences(eventQueue)
scheduleRetryWork()
}
}
// Load queued events from SharedPreferences
private fun loadEventsFromPreferences(): ConcurrentLinkedQueue<AnalyticsEvent> {
val eventsJson = sharedPreferences.getString("events", null)
return if (eventsJson != null) {
val events = gson.fromJson(eventsJson, Array<AnalyticsEvent>::class.java)
ConcurrentLinkedQueue(events.toList())
} else {
ConcurrentLinkedQueue()
}
}
// Save events to SharedPreferences for persistence
private fun saveEventsToPreferences(queue: ConcurrentLinkedQueue<AnalyticsEvent>) {
val eventsJson = gson.toJson(queue.toList())
sharedPreferences.edit().putString("events", eventsJson).apply()
}
// Schedule retry using WorkManager if rate limits are exceeded
private fun scheduleRetryWork() {
val retryWorkRequest = OneTimeWorkRequestBuilder<RetryWorker>()
.setInitialDelay(rateLimiter.getCooldownTime(), TimeUnit.MILLISECONDS)
.build()
workManager.enqueueUniqueWork("RetryInstanaLogging", ExistingWorkPolicy.KEEP, retryWorkRequest)
}
// Rate limiter for tracking logging frequency and enforcing limits within the current session
inner class RateLimiter(
private val maxPerTenMinutes: Int,
private val maxPerTenSeconds: Int
) {
private val tenMinuteWindow = ArrayDeque<Long>()
private val tenSecondWindow = ArrayDeque<Long>()
// Session-based rate limiting, reset on app restart
init {
tenMinuteWindow.clear()
tenSecondWindow.clear()
}
fun canLogEvent(): Boolean {
val currentTime = System.currentTimeMillis()
tenMinuteWindow.removeIf { currentTime - it > 10 * 60 * 1000 }
tenSecondWindow.removeIf { currentTime - it > 10 * 1000 }
return if (tenMinuteWindow.size < maxPerTenMinutes && tenSecondWindow.size < maxPerTenSeconds) {
tenMinuteWindow.add(currentTime)
tenSecondWindow.add(currentTime)
true
} else {
false
}
}
fun getCooldownTime(): Long {
val tenSecondCooldown = tenSecondWindow.peek()?.let { it + 10 * 1000 - System.currentTimeMillis() } ?: 0
val tenMinuteCooldown = tenMinuteWindow.peek()?.let { it + 10 * 60 * 1000 - System.currentTimeMillis() } ?: 0
return maxOf(tenSecondCooldown, tenMinuteCooldown, 1000)
}
}
}Step 4: Implement a RetryWorker for Queued Events
RetryWorker It handles retry attempts for events in the queue, retrying when rate limits are reset.
class RetryWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {
private val rateLimitedLogger = RateLimitedLogger(appContext)
override fun doWork(): Result {
val eventQueue = rateLimitedLogger.loadEventsFromPreferences()
while (eventQueue.isNotEmpty()) {
val event = eventQueue.poll()
if (!rateLimitedLogger.rateLimiter.canLogEvent()) {
eventQueue.add(event)
rateLimitedLogger.saveEventsToPreferences(eventQueue)
rateLimitedLogger.scheduleRetryWork()
return Result.retry()
}
rateLimitedLogger.sendToInstana(event)
}
rateLimitedLogger.saveEventsToPreferences(ConcurrentLinkedQueue())
return Result.success()
}
}Step 5: Using Kotlin Delegation in Base Classes
To integrate this solution in multiple classes, delegate AnalyticsHandler in BaseFragment:
abstract class BaseFragment : Fragment(), AnalyticsHandler by RateLimitedLogger(requireContext()) {
override fun onResume() {
super.onResume()
logScreenView(this::class.java.simpleName)
}
}Key Benefits of This Approach
- Reliable Event Logging: Queues events when rate limits are hit, so data isn’t lost.
- Persistence: Events persist if the app is killed or backgrounded.
- Modular Code with Kotlin Delegation: Clean and testable code by offloading logging logic.
- Reusability: This can be adapted for any event-logging framework.
Wrapping Up
This Kotlin-based solution ensures reliable Instana event logging in Android apps. With rate limits, persistence, and modularity in mind, this guide should help you confidently implement robust analytics. Try it out in your app to ensure no data is missed!

