Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions posthog-android/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- add evaluation tags to android SDK ([#301](https://github.com/PostHog/posthog-android/pull/301))
- feat: add manual captureException ([#300](https://github.com/PostHog/posthog-android/issues/300))
- feat: add exception autocapture ([#305](https://github.com/PostHog/posthog-android/issues/305))

## 3.23.0 - 2025-10-06

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class MyApp : Application() {
captureDeepLinks = false
captureApplicationLifecycleEvents = false
captureScreenViews = false
sessionReplay = true
preloadFeatureFlags = true
sessionReplay = false
preloadFeatureFlags = false
onFeatureFlags = PostHogOnFeatureFlags { print("feature flags loaded") }
addBeforeSend { event ->
if (event.event == "test_name") {
Expand All @@ -41,6 +41,7 @@ class MyApp : Application() {
sessionReplayConfig.captureLogcat = true
sessionReplayConfig.screenshot = true
surveys = true
exceptionAutocapture = true
}
PostHogAndroid.setup(this, config)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ class NormalActivity : ComponentActivity() {
// finish()
// Check if the "enable_network_request" feature flag is enabled

try {
throw RuntimeException("Test error")
} catch (e: Throwable) {
PostHog.captureException(e, mapOf("am-i-stupid" to true))
var str: String? = null
if (str!!.startsWith("")) {
str = "123"
Toast.makeText(this, str, Toast.LENGTH_SHORT).show()
}

val isNetworkRequestEnabled = PostHog.isFeatureEnabled("enable_network_request", false)
Expand Down
1 change: 1 addition & 0 deletions posthog/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Next

- feat: add manual captureException ([#300](https://github.com/PostHog/posthog-android/issues/300))
- feat: add exception autocapture ([#305](https://github.com/PostHog/posthog-android/issues/305))

## 4.0.0 - 2025-10-03

Expand Down
16 changes: 14 additions & 2 deletions posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ public class com/posthog/PostHogConfig {
public static final field DEFAULT_HOST Ljava/lang/String;
public static final field DEFAULT_US_ASSETS_HOST Ljava/lang/String;
public static final field DEFAULT_US_HOST Ljava/lang/String;
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ZLjava/net/Proxy;Lcom/posthog/surveys/PostHogSurveysConfig;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ZLjava/net/Proxy;Lcom/posthog/surveys/PostHogSurveysConfig;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ZLjava/net/Proxy;Lcom/posthog/surveys/PostHogSurveysConfig;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Ljava/util/List;Z)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZZZIZLjava/util/List;ZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;ZLcom/posthog/PersonProfiles;ZLjava/net/Proxy;Lcom/posthog/surveys/PostHogSurveysConfig;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Ljava/util/List;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addBeforeSend (Lcom/posthog/PostHogBeforeSend;)V
public final fun addIntegration (Lcom/posthog/PostHogIntegration;)V
public final fun getApiKey ()Ljava/lang/String;
Expand All @@ -98,6 +98,7 @@ public class com/posthog/PostHogConfig {
public final fun getDebug ()Z
public final fun getEncryption ()Lcom/posthog/PostHogEncryption;
public final fun getEvaluationEnvironments ()Ljava/util/List;
public final fun getExceptionAutocapture ()Z
public final fun getFeatureFlagCalledCacheSize ()I
public final fun getFlushAt ()I
public final fun getFlushIntervalSeconds ()I
Expand Down Expand Up @@ -138,6 +139,7 @@ public class com/posthog/PostHogConfig {
public final fun setDebug (Z)V
public final fun setEncryption (Lcom/posthog/PostHogEncryption;)V
public final fun setEvaluationEnvironments (Ljava/util/List;)V
public final fun setExceptionAutocapture (Z)V
public final fun setFeatureFlagCalledCacheSize (I)V
public final fun setFlushAt (I)V
public final fun setFlushIntervalSeconds (I)V
Expand Down Expand Up @@ -215,6 +217,8 @@ public final class com/posthog/PostHogEvent {
public final fun getType ()Ljava/lang/String;
public final fun getUuid ()Ljava/util/UUID;
public fun hashCode ()I
public final fun isExceptionEvent ()Z
public final fun isFatalExceptionEvent ()Z
public final fun setApiKey (Ljava/lang/String;)V
public fun toString ()Ljava/lang/String;
}
Expand Down Expand Up @@ -385,6 +389,13 @@ public final class com/posthog/PostHogStatelessInterface$DefaultImpls {
public abstract interface annotation class com/posthog/PostHogVisibleForTesting : java/lang/annotation/Annotation {
}

public final class com/posthog/exceptions/PostHogExceptionAutoCaptureIntegration : com/posthog/PostHogIntegration, java/lang/Thread$UncaughtExceptionHandler {
public fun <init> (Lcom/posthog/PostHogConfig;)V
public fun install (Lcom/posthog/PostHogInterface;)V
public fun uncaughtException (Ljava/lang/Thread;Ljava/lang/Throwable;)V
public fun uninstall ()V
}

public final class com/posthog/internal/EvaluationReason {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V
public final fun component1 ()Ljava/lang/String;
Expand Down Expand Up @@ -636,6 +647,7 @@ public final class com/posthog/internal/PostHogUtilsKt {
public static final fun executeSafely (Ljava/util/concurrent/Executor;Ljava/lang/Runnable;)V
public static final fun interruptSafely (Ljava/lang/Thread;)V
public static final fun isNetworkingError (Ljava/lang/Throwable;)Z
public static final fun submitSyncSafely (Ljava/util/concurrent/ExecutorService;Ljava/lang/Runnable;)V
}

public abstract interface class com/posthog/internal/replay/PostHogSessionReplayHandler {
Expand Down
2 changes: 2 additions & 0 deletions posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.posthog

import com.posthog.exceptions.PostHogExceptionAutoCaptureIntegration
import com.posthog.internal.PostHogApi
import com.posthog.internal.PostHogApiEndpoint
import com.posthog.internal.PostHogNoOpLogger
Expand Down Expand Up @@ -134,6 +135,7 @@ public class PostHog private constructor(
}

config.addIntegration(sendCachedEventsIntegration)
config.addIntegration(PostHogExceptionAutoCaptureIntegration(config))

legacyPreferences(config, config.serializer)

Expand Down
9 changes: 9 additions & 0 deletions posthog/src/main/java/com/posthog/PostHogConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ public open class PostHogConfig(
* If this list of package names is empty, all frames will be considered inApp
*/
public val inAppIncludes: MutableList<String> = mutableListOf(),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok breaking change since the manual capture isnt released yet

/**
* Enable autocapture of exceptions
* This feature installs an uncaught exception handler (Thread.UncaughtExceptionHandler) that will capture exceptions
*
* Disabled by default
*
* You can manually capture exceptions by calling [PostHog.captureException]
*/
public var exceptionAutocapture: Boolean = false,
) {
@PostHogInternal
public var logger: PostHogLogger = PostHogNoOpLogger()
Expand Down
18 changes: 17 additions & 1 deletion posthog/src/main/java/com/posthog/PostHogEvent.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.posthog

import com.google.gson.annotations.SerializedName
import com.posthog.internal.exceptions.ThrowableCoercer.Companion.EXCEPTION_LEVEL_ATTRIBUTE
import com.posthog.internal.exceptions.ThrowableCoercer.Companion.EXCEPTION_LEVEL_FATAL
import com.posthog.vendor.uuid.TimeBasedEpochGenerator
import java.util.Date
import java.util.UUID
Expand Down Expand Up @@ -35,4 +37,18 @@ public data class PostHogEvent(
// Only used for Replay
@SerializedName("api_key")
var apiKey: String? = null,
)
) {
/**
* Checks if the event is an exception event ($exception)
*/
public fun isExceptionEvent(): Boolean {
return event == PostHogEventName.EXCEPTION.event
}

/**
* Checks if the event is a fatal exception event ($exception) and properties ($exception_level=fatal)
*/
public fun isFatalExceptionEvent(): Boolean {
return isExceptionEvent() && properties?.get(EXCEPTION_LEVEL_ATTRIBUTE) == EXCEPTION_LEVEL_FATAL
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.posthog.exceptions

import com.posthog.PostHogConfig
import com.posthog.PostHogIntegration
import com.posthog.PostHogInterface
import com.posthog.internal.exceptions.PostHogThrowable
import com.posthog.internal.exceptions.UncaughtExceptionHandlerAdapter

public class PostHogExceptionAutoCaptureIntegration : PostHogIntegration, Thread.UncaughtExceptionHandler {
private val config: PostHogConfig
private val adapterExceptionHandler: UncaughtExceptionHandlerAdapter
private var defaultExceptionHandler: Thread.UncaughtExceptionHandler? = null
private var postHog: PostHogInterface? = null

public constructor(config: PostHogConfig) {
this.config = config
this.adapterExceptionHandler = UncaughtExceptionHandlerAdapter.Adapter.getInstance()
}

internal constructor(config: PostHogConfig, adapterExceptionHandler: UncaughtExceptionHandlerAdapter) {
this.config = config
this.adapterExceptionHandler = adapterExceptionHandler
}

private companion object {
@Volatile
private var integrationInstalled = false
}

override fun install(postHog: PostHogInterface) {
if (integrationInstalled) {
return
}
this.postHog = postHog

if (!config.exceptionAutocapture) {
return
}

val currentExceptionHandler = adapterExceptionHandler.getDefaultUncaughtExceptionHandler()

if (currentExceptionHandler != null) {
if (currentExceptionHandler !is PostHogExceptionAutoCaptureIntegration) {
defaultExceptionHandler = currentExceptionHandler
}
} else {
defaultExceptionHandler = null
}
adapterExceptionHandler.setDefaultUncaughtExceptionHandler(this)

integrationInstalled = true
config.logger.log("Exception autocapture is enabled.")
}

override fun uninstall() {
if (!integrationInstalled) {
return
}
adapterExceptionHandler.setDefaultUncaughtExceptionHandler(defaultExceptionHandler)
integrationInstalled = false
config.logger.log("Exception autocapture is disabled.")
}

override fun uncaughtException(
thread: Thread,
throwable: Throwable,
) {
postHog?.let { postHog ->
postHog.captureException(PostHogThrowable(throwable, thread))
postHog.flush()
}

defaultExceptionHandler?.uncaughtException(thread, throwable)
}
}
109 changes: 69 additions & 40 deletions posthog/src/main/java/com/posthog/internal/PostHogQueue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,60 +48,89 @@ internal class PostHogQueue(

private val delay: Long get() = (config.flushIntervalSeconds * 1000).toLong()

override fun add(event: PostHogEvent) {
executor.executeSafely {
var removeFirst = false
if (deque.size >= config.maxQueueSize) {
removeFirst = true
private fun addEventSync(event: PostHogEvent): Boolean {
storagePrefix?.let {
val dir = File(it, config.apiKey)

if (!dirCreated) {
dir.mkdirs()
dirCreated = true
}

if (removeFirst) {
try {
val first: File
synchronized(dequeLock) {
first = deque.removeFirst()
}
first.deleteSafely(config)
config.logger.log("Queue is full, the oldest event ${first.name} is dropped.")
} catch (ignored: NoSuchElementException) {
val uuid = event.uuid ?: TimeBasedEpochGenerator.generate()
val file = File(dir, "$uuid.event")
synchronized(dequeLock) {
deque.add(file)
}

try {
val os = config.encryption?.encrypt(file.outputStream()) ?: file.outputStream()
os.use { theOutputStream ->
config.serializer.serialize(event, theOutputStream.writer().buffered())
}
config.logger.log("Queued Event ${event.event}: ${file.name}.")

return true
} catch (e: Throwable) {
config.logger.log("Event ${event.event}: ${file.name} failed to parse: $e.")

// if for some reason the file failed to serialize, lets delete it
file.deleteSafely(config)
}

storagePrefix?.let {
val dir = File(it, config.apiKey)
return false
}

if (!dirCreated) {
dir.mkdirs()
dirCreated = true
}
// if there's no storagePrefix, we assume it failed
return true
}

private fun removeEventSync() {
var removeFirst = false
if (deque.size >= config.maxQueueSize) {
removeFirst = true
}

val uuid = event.uuid ?: TimeBasedEpochGenerator.generate()
val file = File(dir, "$uuid.event")
if (removeFirst) {
try {
val first: File
synchronized(dequeLock) {
deque.add(file)
first = deque.removeFirst()
}
first.deleteSafely(config)
config.logger.log("Queue is full, the oldest event ${first.name} is dropped.")
} catch (ignored: NoSuchElementException) {
}
}
}

try {
val os = config.encryption?.encrypt(file.outputStream()) ?: file.outputStream()
os.use { theOutputStream ->
config.serializer.serialize(event, theOutputStream.writer().buffered())
}
config.logger.log("Queued Event ${event.event}: ${file.name}.")

flushIfOverThreshold()
} catch (e: Throwable) {
config.logger.log("Event ${event.event}: ${file.name} failed to parse: $e.")
private fun flushEventSync(
event: PostHogEvent,
isFatal: Boolean = false,
) {
removeEventSync()
if (addEventSync(event)) {
// this is best effort since we dont know if theres
// enough time to flush events to the wire
flushIfOverThreshold(isFatal)
}
}

// if for some reason the file failed to serialize, lets delete it
file.deleteSafely(config)
}
override fun add(event: PostHogEvent) {
if (event.isFatalExceptionEvent()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice way of doing this without changing public API 👍

executor.submitSyncSafely {
flushEventSync(event, true)
}
} else {
executor.executeSafely {
flushEventSync(event)
Comment on lines +120 to +126
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if its fatal, flush it sync otherwise async

}
}
}

private fun flushIfOverThreshold() {
private fun flushIfOverThreshold(isFatal: Boolean) {
if (isAboveThreshold(config.flushAt)) {
flushBatch()
flushBatch(isFatal)
}
}

Expand Down Expand Up @@ -132,8 +161,8 @@ internal class PostHogQueue(
return events
}

private fun flushBatch() {
if (!canFlushBatch()) {
private fun flushBatch(isFatal: Boolean) {
if (!isFatal && !canFlushBatch()) {
config.logger.log("Cannot flush the Queue.")
return
}
Expand Down
Loading
Loading