Skip to main content

Overview

Proper error handling ensures your app gracefully handles data collection failures while maintaining a smooth user experience. The Kount SDK provides multiple ways to detect and handle errors during the collection process.

Collection Status

The SDK provides a getCollectionStatus() method that returns the current state of data collection.

Available Status Values

import com.kount.api.internal.analytics.entities.CollectionStatus

val status = KountSDK.getCollectionStatus()

when (status) {
    CollectionStatus.COMPLETED.toString() -> {
        // Collection finished successfully
    }
    CollectionStatus.FAILED.toString() -> {
        // Collection encountered an error
    }
    CollectionStatus.IN_PROGRESS.toString() -> {
        // Collection is still running
    }
    CollectionStatus.NOT_STARTED.toString() -> {
        // Collection hasn't been initiated yet
    }
}

Handling Failures in Completion Handlers

The most direct way to handle errors is through the failure completion handler:
KountSDK.collectForSession(
    this,
    { sessionId ->
        // Success path
        Log.d("TAG", "Collection succeeded with session ID: $sessionId")
        proceedWithTransaction(sessionId)
    },
    { sessionId, error ->
        // Failure path - handle the error
        Log.e("TAG", "Collection failed: $error")
        Log.e("TAG", "Session ID: $sessionId")
        Log.e("TAG", "Status: ${KountSDK.getCollectionStatus()}")
        
        // Decide how to proceed
        handleCollectionFailure(sessionId, error)
    }
)

Checking Status on Subsequent Screens

When users navigate to a checkout or payment screen, verify the collection status:
CollectionActivity.kt
class CollectionActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_collection)
        setTitle("Checkout Page")

        val textArea = findViewById<TextView>(R.id.textarea)
        val deviceSessionID = KountSDK.getSessionId()
        textArea.append("Session ID:\n$deviceSessionID\n\n")

        // Check the collection status
        val status = KountSDK.getCollectionStatus()
        textArea.append("Collection Status: $status\n\n")
        
        // Handle failed status
        if (status == CollectionStatus.FAILED.toString()) {
            textArea.append("Error: Collection failed\n")
            textArea.setTextColor(Color.RED)
            
            // Show retry option or warning to user
            showRetryOption()
        }
    }
    
    private fun showRetryOption() {
        AlertDialog.Builder(this)
            .setTitle("Collection Failed")
            .setMessage("Would you like to retry data collection?")
            .setPositiveButton("Retry") { _, _ -> retryCollection() }
            .setNegativeButton("Continue Anyway") { _, _ -> /* proceed */ }
            .show()
    }
    
    private fun retryCollection() {
        KountSDK.collectForSession(
            this,
            { sessionId -> 
                Toast.makeText(this, "Collection successful", Toast.LENGTH_SHORT).show()
                recreate() // Refresh the activity
            },
            { sessionId, error ->
                Toast.makeText(this, "Collection failed again", Toast.LENGTH_SHORT).show()
            }
        )
    }
}

Common Error Scenarios

Problem: The device has no internet connection or poor connectivity.Detection:
KountSDK.collectForSession(
    this,
    { sessionId -> /* success */ },
    { sessionId, error ->
        if (error.contains("network", ignoreCase = true) || 
            error.contains("connection", ignoreCase = true)) {
            // Network-related error
            showNetworkError()
        }
    }
)
Handling:
  • Check device connectivity before collecting
  • Implement exponential backoff for retries
  • Allow users to proceed with a warning
Problem: User denied location or other required permissions.Detection:
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    
    if (requestCode == KountSDK.REQUEST_PERMISSION_LOCATION) {
        if (grantResults.isEmpty() || 
            grantResults[0] != PackageManager.PERMISSION_GRANTED) {
            // Permission denied
            Log.w("TAG", "Location permission denied")
        }
        
        // Still proceed with collection (degraded data)
        KountSDK.collectForSession(this, { /* ... */ }, { _, _ -> /* ... */ })
    }
}
Handling:
  • Collection still works without permissions (with reduced data)
  • Explain to users why permissions improve security
  • Don’t block user flow if permissions are denied
Problem: SDK not properly configured (missing merchant ID, wrong environment).Detection:
// Verify configuration before collecting
private fun verifySDKConfiguration(): Boolean {
    val merchantId = KountSDK.getMerchantId()
    if (merchantId.isNullOrEmpty() || merchantId == "999999") {
        Log.e("TAG", "Invalid merchant ID: $merchantId")
        return false
    }
    return true
}

// Use it before collection
if (verifySDKConfiguration()) {
    KountSDK.collectForSession(this, { /* ... */ }, { _, _ -> /* ... */ })
} else {
    Log.e("TAG", "SDK not properly configured")
}
Handling:
  • Validate configuration at app startup
  • Use different merchant IDs for test/production
  • Log configuration errors prominently
Problem: Collection is taking longer than expected.Detection:
private var collectionStartTime: Long = 0
private val COLLECTION_TIMEOUT = 30_000L // 30 seconds

private fun startCollectionWithTimeout() {
    collectionStartTime = System.currentTimeMillis()
    
    val handler = Handler(Looper.getMainLooper())
    val timeoutRunnable = Runnable {
        if (KountSDK.getCollectionStatus() == CollectionStatus.IN_PROGRESS.toString()) {
            Log.w("TAG", "Collection timeout exceeded")
            handleTimeout()
        }
    }
    
    handler.postDelayed(timeoutRunnable, COLLECTION_TIMEOUT)
    
    KountSDK.collectForSession(
        this,
        { sessionId ->
            handler.removeCallbacks(timeoutRunnable)
            val duration = System.currentTimeMillis() - collectionStartTime
            Log.d("TAG", "Collection completed in ${duration}ms")
        },
        { sessionId, error ->
            handler.removeCallbacks(timeoutRunnable)
            Log.e("TAG", "Collection failed: $error")
        }
    )
}
Handling:
  • Set reasonable timeout values
  • Log collection duration for monitoring
  • Proceed with transaction if timeout occurs

Retry Strategies

Simple Retry

private var retryCount = 0
private val MAX_RETRIES = 3

private fun collectWithRetry() {
    KountSDK.collectForSession(
        this,
        { sessionId ->
            Log.d("TAG", "Collection succeeded on attempt ${retryCount + 1}")
            retryCount = 0 // Reset for next time
            proceedWithTransaction(sessionId)
        },
        { sessionId, error ->
            Log.e("TAG", "Collection failed on attempt ${retryCount + 1}: $error")
            
            if (retryCount < MAX_RETRIES) {
                retryCount++
                Handler(Looper.getMainLooper()).postDelayed({
                    Log.d("TAG", "Retrying collection (attempt ${retryCount + 1})")
                    collectWithRetry()
                }, 2000L) // Wait 2 seconds before retry
            } else {
                Log.e("TAG", "Max retries exceeded, proceeding anyway")
                retryCount = 0
                proceedWithTransaction(sessionId)
            }
        }
    )
}

Exponential Backoff

private var retryCount = 0
private val MAX_RETRIES = 3

private fun collectWithExponentialBackoff() {
    KountSDK.collectForSession(
        this,
        { sessionId ->
            retryCount = 0
            proceedWithTransaction(sessionId)
        },
        { sessionId, error ->
            if (retryCount < MAX_RETRIES) {
                retryCount++
                // Exponential backoff: 2s, 4s, 8s
                val delayMs = (2000L * (1 shl (retryCount - 1)))
                
                Log.d("TAG", "Retrying in ${delayMs}ms")
                Handler(Looper.getMainLooper()).postDelayed({
                    collectWithExponentialBackoff()
                }, delayMs)
            } else {
                retryCount = 0
                // Proceed or show error to user
                showFinalErrorDialog(sessionId)
            }
        }
    )
}

User-Initiated Retry

private fun showRetryDialog(sessionId: String, error: String) {
    AlertDialog.Builder(this)
        .setTitle("Collection Failed")
        .setMessage("Data collection failed: $error\n\nWould you like to try again?")
        .setPositiveButton("Retry") { _, _ ->
            collectWithRetry()
        }
        .setNegativeButton("Continue Without") { _, _ ->
            // Proceed with existing session ID
            proceedWithTransaction(sessionId)
        }
        .setCancelable(false)
        .show()
}

Logging and Monitoring

private fun collectWithDetailedLogging() {
    val startTime = System.currentTimeMillis()
    Log.d("TAG", "Starting collection at $startTime")
    Log.d("TAG", "Initial status: ${KountSDK.getCollectionStatus()}")
    
    KountSDK.collectForSession(
        this,
        { sessionId ->
            val duration = System.currentTimeMillis() - startTime
            Log.d("TAG", "Collection SUCCESS")
            Log.d("TAG", "  Session ID: $sessionId")
            Log.d("TAG", "  Duration: ${duration}ms")
            Log.d("TAG", "  Final status: ${KountSDK.getCollectionStatus()}")
            
            // Send metrics to your analytics
            logAnalytics("kount_collection_success", mapOf(
                "duration_ms" to duration,
                "session_id" to sessionId
            ))
        },
        { sessionId, error ->
            val duration = System.currentTimeMillis() - startTime
            Log.e("TAG", "Collection FAILED")
            Log.e("TAG", "  Session ID: $sessionId")
            Log.e("TAG", "  Error: $error")
            Log.e("TAG", "  Duration: ${duration}ms")
            Log.e("TAG", "  Final status: ${KountSDK.getCollectionStatus()}")
            
            // Send error metrics to your analytics
            logAnalytics("kount_collection_failure", mapOf(
                "duration_ms" to duration,
                "error" to error,
                "session_id" to sessionId
            ))
        }
    )
}

Best Practices

Don't Block Users

Even if collection fails, allow users to proceed with their transaction. The session ID is still generated and can be sent to Kount.

Log Everything

Log collection attempts, successes, failures, and durations. This data is invaluable for debugging production issues.

Implement Retries Wisely

Use exponential backoff and limit retry attempts. Don’t retry indefinitely or block the user experience.

Monitor in Production

Track collection success rates and failure patterns. Set up alerts for unusual error rates.

Handle Permissions Gracefully

Don’t force users to grant permissions. Collection works with reduced data if permissions are denied.

Test Error Scenarios

Test your error handling with airplane mode, revoked permissions, and invalid configurations.

Testing Error Conditions

class ErrorTestingActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Test 1: No internet connection
        // (Enable airplane mode before running)
        testNoInternet()
        
        // Test 2: Invalid merchant ID
        testInvalidConfig()
        
        // Test 3: Collection status progression
        testStatusProgression()
    }
    
    private fun testNoInternet() {
        Log.d("TEST", "Testing collection without internet...")
        KountSDK.collectForSession(
            this,
            { sessionId -> Log.d("TEST", "Unexpected success: $sessionId") },
            { sessionId, error -> Log.d("TEST", "Expected failure: $error") }
        )
    }
    
    private fun testInvalidConfig() {
        Log.d("TEST", "Testing with invalid merchant ID...")
        KountSDK.setMerchantId("invalid")
        KountSDK.collectForSession(
            this,
            { sessionId -> Log.d("TEST", "Result: $sessionId") },
            { sessionId, error -> Log.d("TEST", "Error: $error") }
        )
    }
    
    private fun testStatusProgression() {
        Log.d("TEST", "Initial status: ${KountSDK.getCollectionStatus()}")
        
        KountSDK.collectForSession(
            this,
            { sessionId ->
                Log.d("TEST", "Final status: ${KountSDK.getCollectionStatus()}")
            },
            { sessionId, error ->
                Log.d("TEST", "Error status: ${KountSDK.getCollectionStatus()}")
            }
        )
        
        // Check status immediately after call
        Handler(Looper.getMainLooper()).postDelayed({
            Log.d("TEST", "Status after 100ms: ${KountSDK.getCollectionStatus()}")
        }, 100)
    }
}
Always test your error handling in a variety of conditions: airplane mode, poor network, denied permissions, and different Android versions.

Next Steps

Basic Setup

Review the complete basic setup implementation

Build docs developers (and LLMs) love