Files
honeyDueKMP/composeApp/src/androidMain/kotlin/com/tt/honeyDue/analytics/PostHogAnalytics.android.kt
T
Trey T e4dc3ac30b Add PostHog exception capture for crash reporting
Android: uncaught exception handler sends $exception events with stack
trace to PostHog, flushes before delegating to default handler.
iOS: NSSetUncaughtExceptionHandler captures crashes via PostHogSDK,
avoids @MainActor deadlock by calling SDK directly.
Common: captureException() available for non-fatal catches app-wide.
Platform stubs for jvm/js/wasmJs.
2026-03-26 16:49:30 -05:00

132 lines
4.7 KiB
Kotlin

package com.tt.honeyDue.analytics
import android.app.Application
import android.util.Log
import com.posthog.PostHog
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
/**
* Android implementation of PostHog Analytics.
*/
actual object PostHogAnalytics {
// TODO: Replace with your actual PostHog API key
private const val API_KEY = "YOUR_POSTHOG_API_KEY"
private const val HOST = "https://us.i.posthog.com"
private const val TAG = "PostHogAnalytics"
private var isInitialized = false
private var application: Application? = null
/**
* Initialize PostHog SDK with Application context.
* Call this in MainActivity.onCreate() before using other methods.
*/
fun initialize(application: Application, debug: Boolean = false) {
if (isInitialized) return
this.application = application
val config = PostHogAndroidConfig(API_KEY, HOST).apply {
captureScreenViews = false // We'll track screens manually
captureApplicationLifecycleEvents = true
captureDeepLinks = true
this.debug = debug
// Session Replay
sessionReplay = true
sessionReplayConfig.maskAllTextInputs = true
sessionReplayConfig.maskAllImages = false
}
PostHogAndroid.setup(application, config)
isInitialized = true
}
/**
* Initialize from common code (no-op on Android, use initialize(Application) instead)
*/
actual fun initialize() {
// No-op - Android requires Application context, use initialize(Application) instead
}
actual fun identify(userId: String, properties: Map<String, Any>?) {
if (!isInitialized) return
PostHog.identify(userId, userProperties = properties)
}
actual fun capture(event: String, properties: Map<String, Any>?) {
if (!isInitialized) return
PostHog.capture(event, properties = properties)
}
/**
* Capture an exception/crash as a PostHog `$exception` event.
* Uses PostHog's standard exception property names so exceptions
* appear correctly in the PostHog Errors dashboard.
*/
actual fun captureException(throwable: Throwable, properties: Map<String, Any>?) {
if (!isInitialized) return
try {
val exceptionProps = mutableMapOf<String, Any>(
"\$exception_type" to (throwable::class.simpleName ?: "Unknown"),
"\$exception_message" to (throwable.message ?: "No message"),
"\$exception_stack_trace_raw" to throwable.stackTraceToString()
)
if (properties != null) {
exceptionProps.putAll(properties)
}
PostHog.capture("\$exception", properties = exceptionProps)
} catch (e: Exception) {
Log.e(TAG, "Failed to capture exception to PostHog", e)
}
}
actual fun screen(screenName: String, properties: Map<String, Any>?) {
if (!isInitialized) return
PostHog.screen(screenName, properties = properties)
}
actual fun reset() {
if (!isInitialized) return
PostHog.reset()
}
actual fun flush() {
if (!isInitialized) return
PostHog.flush()
}
/**
* Install an uncaught exception handler that captures crashes to PostHog
* before delegating to the default handler (which shows the crash dialog).
* Call this after initialize() in MainActivity.onCreate().
*/
actual fun setupExceptionHandler() {
if (!isInitialized) return
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
try {
PostHog.capture(
event = "\$exception",
properties = mapOf(
"\$exception_type" to (throwable::class.simpleName ?: "Unknown"),
"\$exception_message" to (throwable.message ?: "No message"),
"\$exception_stack_trace_raw" to throwable.stackTraceToString(),
"\$exception_thread" to thread.name,
"\$exception_is_fatal" to true
)
)
// Flush to ensure the event is sent before the process dies
PostHog.flush()
} catch (_: Exception) {
// Don't let our crash handler crash
}
// Call the default handler so the system crash dialog still appears
defaultHandler?.uncaughtException(thread, throwable)
}
Log.d(TAG, "Uncaught exception handler installed")
}
}