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.
This commit is contained in:
@@ -80,6 +80,9 @@ class MainActivity : FragmentActivity(), SingletonImageLoader.Factory {
|
|||||||
// Initialize PostHog Analytics
|
// Initialize PostHog Analytics
|
||||||
PostHogAnalytics.initialize(application, debug = true) // Set debug=false for release
|
PostHogAnalytics.initialize(application, debug = true) // Set debug=false for release
|
||||||
|
|
||||||
|
// Install uncaught exception handler to capture crashes to PostHog
|
||||||
|
PostHogAnalytics.setupExceptionHandler()
|
||||||
|
|
||||||
// Handle deep link, notification navigation, and file import from intent
|
// Handle deep link, notification navigation, and file import from intent
|
||||||
handleDeepLink(intent)
|
handleDeepLink(intent)
|
||||||
handleNotificationNavigation(intent)
|
handleNotificationNavigation(intent)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.tt.honeyDue.analytics
|
package com.tt.honeyDue.analytics
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
import com.posthog.PostHog
|
import com.posthog.PostHog
|
||||||
import com.posthog.android.PostHogAndroid
|
import com.posthog.android.PostHogAndroid
|
||||||
import com.posthog.android.PostHogAndroidConfig
|
import com.posthog.android.PostHogAndroidConfig
|
||||||
@@ -13,6 +14,8 @@ actual object PostHogAnalytics {
|
|||||||
private const val API_KEY = "YOUR_POSTHOG_API_KEY"
|
private const val API_KEY = "YOUR_POSTHOG_API_KEY"
|
||||||
private const val HOST = "https://us.i.posthog.com"
|
private const val HOST = "https://us.i.posthog.com"
|
||||||
|
|
||||||
|
private const val TAG = "PostHogAnalytics"
|
||||||
|
|
||||||
private var isInitialized = false
|
private var isInitialized = false
|
||||||
private var application: Application? = null
|
private var application: Application? = null
|
||||||
|
|
||||||
@@ -57,6 +60,28 @@ actual object PostHogAnalytics {
|
|||||||
PostHog.capture(event, properties = properties)
|
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>?) {
|
actual fun screen(screenName: String, properties: Map<String, Any>?) {
|
||||||
if (!isInitialized) return
|
if (!isInitialized) return
|
||||||
PostHog.screen(screenName, properties = properties)
|
PostHog.screen(screenName, properties = properties)
|
||||||
@@ -71,4 +96,36 @@ actual object PostHogAnalytics {
|
|||||||
if (!isInitialized) return
|
if (!isInitialized) return
|
||||||
PostHog.flush()
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ expect object PostHogAnalytics {
|
|||||||
fun initialize()
|
fun initialize()
|
||||||
fun identify(userId: String, properties: Map<String, Any>? = null)
|
fun identify(userId: String, properties: Map<String, Any>? = null)
|
||||||
fun capture(event: String, properties: Map<String, Any>? = null)
|
fun capture(event: String, properties: Map<String, Any>? = null)
|
||||||
|
fun captureException(throwable: Throwable, properties: Map<String, Any>? = null)
|
||||||
fun screen(screenName: String, properties: Map<String, Any>? = null)
|
fun screen(screenName: String, properties: Map<String, Any>? = null)
|
||||||
fun reset()
|
fun reset()
|
||||||
fun flush()
|
fun flush()
|
||||||
|
fun setupExceptionHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ actual object PostHogAnalytics {
|
|||||||
// iOS uses Swift PostHogAnalytics.shared.capture() directly
|
// iOS uses Swift PostHogAnalytics.shared.capture() directly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun captureException(throwable: Throwable, properties: Map<String, Any>?) {
|
||||||
|
// iOS exception capture is done in Swift via AnalyticsManager
|
||||||
|
}
|
||||||
|
|
||||||
actual fun screen(screenName: String, properties: Map<String, Any>?) {
|
actual fun screen(screenName: String, properties: Map<String, Any>?) {
|
||||||
// iOS uses Swift PostHogAnalytics.shared.screen() directly
|
// iOS uses Swift PostHogAnalytics.shared.screen() directly
|
||||||
}
|
}
|
||||||
@@ -29,4 +33,8 @@ actual object PostHogAnalytics {
|
|||||||
actual fun flush() {
|
actual fun flush() {
|
||||||
// iOS uses Swift PostHogAnalytics.shared.flush() directly
|
// iOS uses Swift PostHogAnalytics.shared.flush() directly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun setupExceptionHandler() {
|
||||||
|
// iOS exception handler is set up in Swift via AnalyticsManager.setupExceptionHandler()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ actual object PostHogAnalytics {
|
|||||||
// No-op for web
|
// No-op for web
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun captureException(throwable: Throwable, properties: Map<String, Any>?) {
|
||||||
|
// No-op for web
|
||||||
|
}
|
||||||
|
|
||||||
actual fun screen(screenName: String, properties: Map<String, Any>?) {
|
actual fun screen(screenName: String, properties: Map<String, Any>?) {
|
||||||
// No-op for web
|
// No-op for web
|
||||||
}
|
}
|
||||||
@@ -28,4 +32,8 @@ actual object PostHogAnalytics {
|
|||||||
actual fun flush() {
|
actual fun flush() {
|
||||||
// No-op for web
|
// No-op for web
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun setupExceptionHandler() {
|
||||||
|
// No-op for web
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ actual object PostHogAnalytics {
|
|||||||
// No-op for desktop
|
// No-op for desktop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun captureException(throwable: Throwable, properties: Map<String, Any>?) {
|
||||||
|
// No-op for desktop
|
||||||
|
}
|
||||||
|
|
||||||
actual fun screen(screenName: String, properties: Map<String, Any>?) {
|
actual fun screen(screenName: String, properties: Map<String, Any>?) {
|
||||||
// No-op for desktop
|
// No-op for desktop
|
||||||
}
|
}
|
||||||
@@ -28,4 +32,8 @@ actual object PostHogAnalytics {
|
|||||||
actual fun flush() {
|
actual fun flush() {
|
||||||
// No-op for desktop
|
// No-op for desktop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun setupExceptionHandler() {
|
||||||
|
// No-op for desktop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ actual object PostHogAnalytics {
|
|||||||
// No-op for web
|
// No-op for web
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun captureException(throwable: Throwable, properties: Map<String, Any>?) {
|
||||||
|
// No-op for web
|
||||||
|
}
|
||||||
|
|
||||||
actual fun screen(screenName: String, properties: Map<String, Any>?) {
|
actual fun screen(screenName: String, properties: Map<String, Any>?) {
|
||||||
// No-op for web
|
// No-op for web
|
||||||
}
|
}
|
||||||
@@ -28,4 +32,8 @@ actual object PostHogAnalytics {
|
|||||||
actual fun flush() {
|
actual fun flush() {
|
||||||
// No-op for web
|
// No-op for web
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun setupExceptionHandler() {
|
||||||
|
// No-op for web
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,47 @@ final class AnalyticsManager {
|
|||||||
PostHogSDK.shared.capture("screen_viewed", properties: props)
|
PostHogSDK.shared.capture("screen_viewed", properties: props)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Exception / Crash Capture
|
||||||
|
|
||||||
|
/// Capture a Swift Error as a PostHog `$exception` event.
|
||||||
|
func captureException(_ error: Error, properties: [String: Any]? = nil) {
|
||||||
|
guard isConfigured else { return }
|
||||||
|
var props: [String: Any] = [
|
||||||
|
"$exception_type": String(describing: type(of: error)),
|
||||||
|
"$exception_message": error.localizedDescription,
|
||||||
|
"$exception_stack_trace_raw": Thread.callStackSymbols.joined(separator: "\n")
|
||||||
|
]
|
||||||
|
if let properties { props.merge(properties) { _, new in new } }
|
||||||
|
PostHogSDK.shared.capture("$exception", properties: props)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capture an NSException as a PostHog `$exception` event.
|
||||||
|
func captureNSException(_ exception: NSException, isFatal: Bool = false) {
|
||||||
|
guard isConfigured else { return }
|
||||||
|
PostHogSDK.shared.capture("$exception", properties: [
|
||||||
|
"$exception_type": exception.name.rawValue,
|
||||||
|
"$exception_message": exception.reason ?? "No reason",
|
||||||
|
"$exception_stack_trace_raw": exception.callStackSymbols.joined(separator: "\n"),
|
||||||
|
"$exception_is_fatal": isFatal
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install an uncaught NSException handler that captures crashes to PostHog
|
||||||
|
/// before the app terminates. Call this once after configure().
|
||||||
|
func setupExceptionHandler() {
|
||||||
|
NSSetUncaughtExceptionHandler { exception in
|
||||||
|
// Cannot use AnalyticsManager.shared here (may deadlock on @MainActor),
|
||||||
|
// so call PostHogSDK directly.
|
||||||
|
PostHogSDK.shared.capture("$exception", properties: [
|
||||||
|
"$exception_type": exception.name.rawValue,
|
||||||
|
"$exception_message": exception.reason ?? "No reason",
|
||||||
|
"$exception_stack_trace_raw": exception.callStackSymbols.joined(separator: "\n"),
|
||||||
|
"$exception_is_fatal": true
|
||||||
|
])
|
||||||
|
PostHogSDK.shared.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Flush & Reset
|
// MARK: - Flush & Reset
|
||||||
|
|
||||||
func flush() {
|
func flush() {
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ struct iOSApp: App {
|
|||||||
if !UITestRuntime.isEnabled {
|
if !UITestRuntime.isEnabled {
|
||||||
// Initialize PostHog Analytics (must use Swift AnalyticsManager, not the Kotlin stub)
|
// Initialize PostHog Analytics (must use Swift AnalyticsManager, not the Kotlin stub)
|
||||||
AnalyticsManager.shared.configure()
|
AnalyticsManager.shared.configure()
|
||||||
|
|
||||||
|
// Install uncaught exception handler to capture crashes to PostHog
|
||||||
|
AnalyticsManager.shared.setupExceptionHandler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user