From fc66021d9f0ae61a4213df52ab7deda70511ab5d Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 21 Sep 2023 14:36:54 +0200 Subject: [PATCH] swallow exceptions --- ...PostHogUtils.kt => PostHogAndroidUtils.kt} | 2 +- .../java/com/posthog/android/sample/MyApp.kt | 2 +- posthog-v3/posthog/api/posthog.api | 21 ++- .../src/main/java/com/posthog/PostHog.kt | 127 +++++++++++------- .../main/java/com/posthog/PostHogConfig.kt | 1 - .../main/java/com/posthog/PostHogDataMode.kt | 6 - .../java/com/posthog/PostHogEncryption.kt | 1 - .../posthog/internal/GsonDateTypeAdapter.kt | 4 +- .../internal/GzipRequestInterceptor.kt | 2 +- .../java/com/posthog/internal/PostHogApi.kt | 6 +- .../com/posthog/internal/PostHogApiError.kt | 2 +- .../java/com/posthog/internal/PostHogQueue.kt | 63 ++++++--- .../com/posthog/internal/PostHogSerializer.kt | 14 +- .../java/com/posthog/internal/PostHogUtils.kt | 25 ++++ .../internal/SendCachedEventsIntegration.kt | 100 ++++++++++---- 15 files changed, 244 insertions(+), 132 deletions(-) rename posthog-v3/posthog-android/src/main/java/com/posthog/android/internal/{PostHogUtils.kt => PostHogAndroidUtils.kt} (97%) delete mode 100644 posthog-v3/posthog/src/main/java/com/posthog/PostHogDataMode.kt create mode 100644 posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogUtils.kt diff --git a/posthog-v3/posthog-android/src/main/java/com/posthog/android/internal/PostHogUtils.kt b/posthog-v3/posthog-android/src/main/java/com/posthog/android/internal/PostHogAndroidUtils.kt similarity index 97% rename from posthog-v3/posthog-android/src/main/java/com/posthog/android/internal/PostHogUtils.kt rename to posthog-v3/posthog-android/src/main/java/com/posthog/android/internal/PostHogAndroidUtils.kt index 46b8ec1f..378b21da 100644 --- a/posthog-v3/posthog-android/src/main/java/com/posthog/android/internal/PostHogUtils.kt +++ b/posthog-v3/posthog-android/src/main/java/com/posthog/android/internal/PostHogAndroidUtils.kt @@ -28,7 +28,7 @@ internal fun getPackageInfo( context.packageManager.getPackageInfo(context.packageName, 0) } } catch (e: Throwable) { - config.logger.log("Error getting package info.") + config.logger.log("Error getting package info: $e.") null } } diff --git a/posthog-v3/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt b/posthog-v3/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt index c4ac8f86..4aa0d2f0 100644 --- a/posthog-v3/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt +++ b/posthog-v3/posthog-samples/posthog-android-sample/src/main/java/com/posthog/android/sample/MyApp.kt @@ -12,7 +12,7 @@ class MyApp : Application() { debug = true // flushAt = 5 // flushIntervalSeconds = 5 - flushAt = 5 + flushAt = 1 } PostHogAndroid.setup(this, config) diff --git a/posthog-v3/posthog/api/posthog.api b/posthog-v3/posthog/api/posthog.api index 8bc1348c..d0f6d39d 100644 --- a/posthog-v3/posthog/api/posthog.api +++ b/posthog-v3/posthog/api/posthog.api @@ -25,8 +25,10 @@ public final class com/posthog/PostHog { } public final class com/posthog/PostHog$Companion { - public final fun capture (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;)V - public static synthetic fun capture$default (Lcom/posthog/PostHog$Companion;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)V + public final fun alias (Ljava/lang/String;Ljava/util/Map;)V + public static synthetic fun alias$default (Lcom/posthog/PostHog$Companion;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V + public final fun capture (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V + public static synthetic fun capture$default (Lcom/posthog/PostHog$Companion;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)V public final fun close ()V public final fun flush ()V public final fun getFeatureFlag (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object; @@ -43,17 +45,18 @@ public final class com/posthog/PostHog$Companion { public final fun optOut ()V public final fun reloadFeatureFlagsRequest ()V public final fun reset ()V + public final fun screen (Ljava/lang/String;Ljava/util/Map;)V + public static synthetic fun screen$default (Lcom/posthog/PostHog$Companion;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V public final fun setup (Lcom/posthog/PostHogConfig;)V public final fun with (Lcom/posthog/PostHogConfig;)Lcom/posthog/PostHog; } public final class com/posthog/PostHogConfig { - public fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogDataMode;Lcom/posthog/PostHogEncryption;Ljava/util/List;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogDataMode;Lcom/posthog/PostHogEncryption;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Ljava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getApiKey ()Ljava/lang/String; public final fun getCachePreferences ()Lcom/posthog/PostHogPreferences; public final fun getContext ()Lcom/posthog/PostHogContext; - public final fun getDataMode ()Lcom/posthog/PostHogDataMode; public final fun getDebug ()Z public final fun getEncryption ()Lcom/posthog/PostHogEncryption; public final fun getFlushAt ()I @@ -73,7 +76,6 @@ public final class com/posthog/PostHogConfig { public final fun getStoragePrefix ()Ljava/lang/String; public final fun setCachePreferences (Lcom/posthog/PostHogPreferences;)V public final fun setContext (Lcom/posthog/PostHogContext;)V - public final fun setDataMode (Lcom/posthog/PostHogDataMode;)V public final fun setDebug (Z)V public final fun setEncryption (Lcom/posthog/PostHogEncryption;)V public final fun setFlushAt (I)V @@ -96,13 +98,6 @@ public abstract interface class com/posthog/PostHogContext { public abstract fun getStaticContext ()Ljava/util/Map; } -public final class com/posthog/PostHogDataMode : java/lang/Enum { - public static final field ANY Lcom/posthog/PostHogDataMode; - public static final field WIFI Lcom/posthog/PostHogDataMode; - public static fun valueOf (Ljava/lang/String;)Lcom/posthog/PostHogDataMode; - public static fun values ()[Lcom/posthog/PostHogDataMode; -} - public abstract interface class com/posthog/PostHogEncryption { public abstract fun decrypt (Ljava/io/InputStream;)Ljava/io/InputStream; public abstract fun encrypt (Ljava/io/OutputStream;)Ljava/io/OutputStream; diff --git a/posthog-v3/posthog/src/main/java/com/posthog/PostHog.kt b/posthog-v3/posthog/src/main/java/com/posthog/PostHog.kt index 9128a951..d54e5a63 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/PostHog.kt @@ -17,52 +17,58 @@ public class PostHog private constructor() { private var config: PostHogConfig? = null -// private var sessionManager: PostHogSessionManager? = null private var featureFlags: PostHogFeatureFlags? = null private var api: PostHogApi? = null private var queue: PostHogQueue? = null public fun setup(config: PostHogConfig) { synchronized(lock) { - if (enabled) { - config.logger.log("Setup called despite already being setup!") - return - } + try { + if (enabled) { + config.logger.log("Setup called despite already being setup!") + return + } - val cachePreferences = config.cachePreferences ?: PostHogMemoryPreferences() - config.cachePreferences = cachePreferences -// sessionManager = PostHogSessionManager(cachePreferences) - val serializer = PostHogSerializer(config) - val api = PostHogApi(config, serializer) - val queue = PostHogQueue(config, api, serializer) - val featureFlags = PostHogFeatureFlags(config, api) - - val optOut = config.cachePreferences?.getValue("opt-out", defaultValue = false) as? Boolean - optOut?.let { - config.optOut = optOut - } + val cachePreferences = config.cachePreferences ?: PostHogMemoryPreferences() + config.cachePreferences = cachePreferences + val serializer = PostHogSerializer(config) + val api = PostHogApi(config, serializer) + val queue = PostHogQueue(config, api, serializer) + val featureFlags = PostHogFeatureFlags(config, api) - val startDate = Date() - val sendCachedEventsIntegration = SendCachedEventsIntegration(config, api, serializer, startDate) + val optOut = config.cachePreferences?.getValue("opt-out", defaultValue = false) as? Boolean + optOut?.let { + config.optOut = optOut + } - this.api = api - this.config = config - this.queue = queue - this.featureFlags = featureFlags - enabled = true + val startDate = Date() + val sendCachedEventsIntegration = SendCachedEventsIntegration(config, api, serializer, startDate) - config.integrations.add(sendCachedEventsIntegration) + this.api = api + this.config = config + this.queue = queue + this.featureFlags = featureFlags + enabled = true - legacyPreferences(config, serializer) + config.integrations.add(sendCachedEventsIntegration) - queue.start() + legacyPreferences(config, serializer) - config.integrations.forEach { - it.install() - } + queue.start() - if (config.preloadFeatureFlags) { - loadFeatureFlagsRequest() + config.integrations.forEach { + try { + it.install() + } catch (e: Throwable) { + config.logger.log("Integration ${it.javaClass.name} failed to install: $e.") + } + } + + if (config.preloadFeatureFlags) { + loadFeatureFlagsRequest() + } + } catch (e: Throwable) { + config.logger.log("Setup failed: $e.") } } } @@ -70,31 +76,43 @@ public class PostHog private constructor() { private fun legacyPreferences(config: PostHogConfig, serializer: PostHogSerializer) { val cachedPrefs = config.cachePreferences?.getValue(config.apiKey) as? String cachedPrefs?.let { - serializer.deserializeCachedProperties(it)?.let { props -> - val anonymousId = props["anonymousId"] as? String - val distinctId = props["distinctId"] as? String + try { + serializer.deserializeCachedProperties(it)?.let { props -> + val anonymousId = props["anonymousId"] as? String + val distinctId = props["distinctId"] as? String - anonymousId?.let { anon -> - this.anonymousId = anon - } - distinctId?.let { dist -> - this.distinctId = dist - } + anonymousId?.let { anon -> + this.anonymousId = anon + } + distinctId?.let { dist -> + this.distinctId = dist + } - config.cachePreferences?.remove(config.apiKey) + config.cachePreferences?.remove(config.apiKey) + } + } catch (e: Throwable) { + config.logger.log("Legacy cached prefs: $cachedPrefs failed to parse: $e.") } } } public fun close() { synchronized(lock) { - enabled = false + try { + enabled = false + + config?.integrations?.forEach { + try { + it.uninstall() + } catch (e: Throwable) { + config?.logger?.log("Integration ${it.javaClass.name} failed to uninstall: $e.") + } + } - config?.integrations?.forEach { - it.uninstall() + queue?.stop() + } catch (e: Throwable) { + config?.logger?.log("Close failed: $e.") } - - queue?.stop() } } @@ -330,8 +348,6 @@ public class PostHog private constructor() { queue?.flush() } - // TODO: If you also want to reset the device_id so that the device will be considered a new device in future events, you can pass true as an argument: - // this does not exist on Android yet public fun reset() { if (!isEnabled()) { return @@ -385,8 +401,8 @@ public class PostHog private constructor() { shared.close() } - public fun capture(event: String, properties: Map? = null, userProperties: Map? = null) { - shared.capture(event, properties = properties, userProperties = userProperties) + public fun capture(event: String, properties: Map? = null, userProperties: Map? = null, groupProperties: Map? = null) { + shared.capture(event, properties = properties, userProperties = userProperties, groupProperties = groupProperties) } public fun identify(distinctId: String, properties: Map? = null, userProperties: Map? = null) { @@ -428,6 +444,13 @@ public class PostHog private constructor() { public fun group(type: String, key: String, groupProperties: Map? = null) { shared.group(type, key, groupProperties = groupProperties) } - // TODO: add other methods + + public fun screen(screenTitle: String, properties: Map? = null) { + shared.screen(screenTitle, properties = properties) + } + + public fun alias(alias: String, properties: Map? = null) { + shared.alias(alias, properties = properties) + } } } diff --git a/posthog-v3/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog-v3/posthog/src/main/java/com/posthog/PostHogConfig.kt index aafa3e08..6db4b542 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/PostHogConfig.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/PostHogConfig.kt @@ -18,7 +18,6 @@ public class PostHogConfig( // (30).toDuration(DurationUnit.SECONDS) requires Kotlin 1.6 public var flushIntervalSeconds: Int = 30, - public var dataMode: PostHogDataMode = PostHogDataMode.ANY, public var encryption: PostHogEncryption? = null, public val integrations: MutableList = mutableListOf(), ) { diff --git a/posthog-v3/posthog/src/main/java/com/posthog/PostHogDataMode.kt b/posthog-v3/posthog/src/main/java/com/posthog/PostHogDataMode.kt deleted file mode 100644 index 38d4ee1a..00000000 --- a/posthog-v3/posthog/src/main/java/com/posthog/PostHogDataMode.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.posthog - -public enum class PostHogDataMode { - WIFI, - ANY, -} diff --git a/posthog-v3/posthog/src/main/java/com/posthog/PostHogEncryption.kt b/posthog-v3/posthog/src/main/java/com/posthog/PostHogEncryption.kt index 0ed7f522..0ecf99bb 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/PostHogEncryption.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/PostHogEncryption.kt @@ -3,7 +3,6 @@ package com.posthog import java.io.InputStream import java.io.OutputStream -// TODO: call PostHogEncryption public interface PostHogEncryption { public fun decrypt(inputStream: InputStream): InputStream diff --git a/posthog-v3/posthog/src/main/java/com/posthog/internal/GsonDateTypeAdapter.kt b/posthog-v3/posthog/src/main/java/com/posthog/internal/GsonDateTypeAdapter.kt index 66fb2d88..cdd6ae2c 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/internal/GsonDateTypeAdapter.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/internal/GsonDateTypeAdapter.kt @@ -18,7 +18,7 @@ internal class GsonDateTypeAdapter(private val config: PostHogConfig) : JsonDese return try { ISO8601Utils.parse(json?.asString, ParsePosition(0)) } catch (e: Throwable) { - config.logger.log("${json?.asString} isn't a deserializable ISO8601 Date.") + config.logger.log("${json?.asString} isn't a deserializable ISO8601 Date: $e.") null } } @@ -28,7 +28,7 @@ internal class GsonDateTypeAdapter(private val config: PostHogConfig) : JsonDese val dateStr = ISO8601Utils.format(src, true) JsonPrimitive(dateStr) } catch (e: Throwable) { - config.logger.log("$src isn't a serializable ISO8601 Date.") + config.logger.log("$src isn't a serializable ISO8601 Date: $e.") null } } diff --git a/posthog-v3/posthog/src/main/java/com/posthog/internal/GzipRequestInterceptor.kt b/posthog-v3/posthog/src/main/java/com/posthog/internal/GzipRequestInterceptor.kt index d35195da..196c7cd4 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/internal/GzipRequestInterceptor.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/internal/GzipRequestInterceptor.kt @@ -50,7 +50,7 @@ internal class GzipRequestInterceptor(private val config: PostHogConfig) : Inter .method(originalRequest.method, gzip(body)) .build() } catch (e: Throwable) { - config.logger.log("Failed to gzip the request body.") + config.logger.log("Failed to gzip the request body: $e.") originalRequest } diff --git a/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogApi.kt b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogApi.kt index 078a55ae..86c731d5 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogApi.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogApi.kt @@ -7,6 +7,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okio.BufferedSink +import java.io.IOException import java.io.OutputStream import java.util.Date @@ -24,7 +25,7 @@ internal class PostHogApi(private val config: PostHogConfig, private val seriali } } - @Throws(PostHogApiError::class) + @Throws(PostHogApiError::class, IOException::class) fun batch(events: List) { val batch = PostHogBatchEvent(config.apiKey, events) @@ -54,7 +55,7 @@ internal class PostHogApi(private val config: PostHogConfig, private val seriali .build() } - @Throws(PostHogApiError::class) + @Throws(PostHogApiError::class, IOException::class) fun decide(properties: Map): Map? { val map = properties.toMutableMap() map["token"] = config.apiKey @@ -64,7 +65,6 @@ internal class PostHogApi(private val config: PostHogConfig, private val seriali } client.newCall(request).execute().use { - // TODO: do we handle 429 differently? if (!it.isSuccessful) throw PostHogApiError(it.code, it.message, body = it.body) it.body?.let { body -> diff --git a/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogApiError.kt b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogApiError.kt index 89b53625..c1966ba5 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogApiError.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogApiError.kt @@ -6,5 +6,5 @@ import java.lang.RuntimeException internal class PostHogApiError( val statusCode: Int, override val message: String, - body: ResponseBody? = null, + val body: ResponseBody? = null, ) : RuntimeException(message) diff --git a/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogQueue.kt b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogQueue.kt index b648b897..a782f754 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogQueue.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogQueue.kt @@ -3,6 +3,7 @@ package com.posthog.internal import com.posthog.PostHogConfig import com.posthog.PostHogEvent import java.io.File +import java.io.IOException import java.util.Calendar import java.util.Date import java.util.Timer @@ -67,11 +68,15 @@ internal class PostHogQueue(private val config: PostHogConfig, private val api: deque.add(file) } - val os = config.encryption?.encrypt(file.outputStream()) ?: file.outputStream() - serializer.serializeEvent(event, os.writer().buffered()) - config.logger.log("Queued event ${file.name}.") + try { + val os = config.encryption?.encrypt(file.outputStream()) ?: file.outputStream() + serializer.serializeEvent(event, os.writer().buffered()) + config.logger.log("Queued event ${file.name}.") - flushIfOverThreshold() + flushIfOverThreshold() + } catch (e: Throwable) { + config.logger.log("Event ${event.event} failed to parse: $e.") + } } } } @@ -120,7 +125,7 @@ internal class PostHogQueue(private val config: PostHogConfig, private val api: batchEvents() retryCount = 0 } catch (e: Throwable) { - config.logger.log("Flushing failed: $e") + config.logger.log("Flushing failed: $e.") retry = true retryCount++ @@ -137,22 +142,46 @@ internal class PostHogQueue(private val config: PostHogConfig, private val api: val events = mutableListOf() for (file in files) { - val inputStream = config.encryption?.decrypt(file.inputStream()) ?: file.inputStream() + try { + val inputStream = config.encryption?.decrypt(file.inputStream()) ?: file.inputStream() - val event = serializer.deserializeEvent(inputStream.reader().buffered()) - event?.let { - events.add(it) + val event = serializer.deserializeEvent(inputStream.reader().buffered()) + event?.let { + events.add(it) + } + } catch (e: Throwable) { + synchronized(dequeLock) { + deque.remove(file) + } + file.delete() + config.logger.log("File: ${file.name} failed to parse: $e.") } } - api.batch(events) - - synchronized(dequeLock) { - deque.removeAll(files) - } + var deleteFiles = true + try { + api.batch(events) + } catch (e: PostHogApiError) { + if (e.statusCode >= 400) { + // TODO: the reason to delete or not the files? + } + throw e + } catch (e: IOException) { + // no connection should try again + if (e.isNetworkingError()) { + deleteFiles = false + } + throw e + } finally { + if (deleteFiles) { + synchronized(dequeLock) { + deque.removeAll(files) + } - files.forEach { - it.delete() + files.forEach { + it.delete() + } + } } } @@ -179,7 +208,7 @@ internal class PostHogQueue(private val config: PostHogConfig, private val api: } retryCount = 0 } catch (e: Throwable) { - config.logger.log("Flushing failed: $e") + config.logger.log("Flushing failed: $e.") retry = true retryCount++ } finally { diff --git a/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogSerializer.kt b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogSerializer.kt index bce74c68..15cc4e94 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogSerializer.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogSerializer.kt @@ -2,9 +2,11 @@ package com.posthog.internal import com.google.gson.GsonBuilder import com.google.gson.JsonIOException +import com.google.gson.JsonSyntaxException import com.google.gson.reflect.TypeToken import com.posthog.PostHogConfig import com.posthog.PostHogEvent +import java.io.IOException import java.io.Reader import java.io.Writer import java.util.Date @@ -18,35 +20,35 @@ internal class PostHogSerializer(private val config: PostHogConfig) { private val gsonEventType = object : TypeToken() {}.type private val gsonMapType = object : TypeToken>() {}.type - @Throws(JsonIOException::class) + @Throws(JsonIOException::class, IOException::class) fun serializeEvent(event: PostHogEvent, writer: Writer) { gson.toJson(event, gsonEventType, writer) writer.flush() } - @Throws(JsonIOException::class) + @Throws(JsonIOException::class, JsonSyntaxException::class) fun deserializeEvent(reader: Reader): PostHogEvent? { return gson.fromJson(reader, gsonEventType) } - @Throws(JsonIOException::class) + @Throws(JsonIOException::class, IOException::class) fun serializeDecideApi(properties: Map, writer: Writer) { gson.toJson(properties, gsonMapType, writer) writer.flush() } - @Throws(JsonIOException::class) + @Throws(JsonIOException::class, JsonSyntaxException::class) fun deserializeDecideApi(reader: Reader): Map? { return gson.fromJson(reader, gsonMapType) } - @Throws(JsonIOException::class) + @Throws(JsonIOException::class, IOException::class) fun serializeBatchApi(batch: PostHogBatchEvent, writer: Writer) { gson.toJson(batch, gsonBatchType, writer) writer.flush() } - @Throws(JsonIOException::class) + @Throws(JsonIOException::class, JsonSyntaxException::class) fun deserializeCachedProperties(json: String): Map? { return gson.fromJson(json, gsonMapType) } diff --git a/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogUtils.kt b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogUtils.kt new file mode 100644 index 00000000..f1baedc5 --- /dev/null +++ b/posthog-v3/posthog/src/main/java/com/posthog/internal/PostHogUtils.kt @@ -0,0 +1,25 @@ +package com.posthog.internal + +import java.io.IOException +import java.io.InterruptedIOException +import java.net.SocketTimeoutException +import java.net.UnknownHostException + +private fun isRequestCanceled(throwable: Throwable): Boolean { + return throwable is IOException && + throwable.message?.contentEquals("Canceled") ?: false || throwable is InterruptedIOException +} + +private fun noInternetAvailable(throwable: Throwable): Boolean { + return throwable is UnknownHostException +} + +private fun isConnectionTimeout(throwable: Throwable): Boolean { + return throwable is SocketTimeoutException +} + +internal fun Throwable.isNetworkingError(): Boolean { + return isConnectionTimeout(this) || + noInternetAvailable(this) || + isRequestCanceled(this) +} diff --git a/posthog-v3/posthog/src/main/java/com/posthog/internal/SendCachedEventsIntegration.kt b/posthog-v3/posthog/src/main/java/com/posthog/internal/SendCachedEventsIntegration.kt index a9e64e68..6c0696d8 100644 --- a/posthog-v3/posthog/src/main/java/com/posthog/internal/SendCachedEventsIntegration.kt +++ b/posthog-v3/posthog/src/main/java/com/posthog/internal/SendCachedEventsIntegration.kt @@ -5,6 +5,7 @@ import com.posthog.PostHogEvent import com.posthog.PostHogIntegration import java.io.File import java.io.FileFilter +import java.io.IOException import java.util.Date import java.util.concurrent.Executors @@ -34,29 +35,54 @@ internal class SendCachedEventsIntegration(private val config: PostHogConfig, pr return } - val legacy = PostHogQueueFile.Builder(legacyFile) - .forceLegacy(true) - .build() + try { + val legacy = PostHogQueueFile.Builder(legacyFile) + .forceLegacy(true) + .build() - val iterator = legacy.iterator() + val iterator = legacy.iterator() - val events = mutableListOf() + val events = mutableListOf() - while (iterator.hasNext()) { - val eventBytes = iterator.next() + while (iterator.hasNext()) { + val eventBytes = iterator.next() - val inputStream = config.encryption?.decrypt(eventBytes.inputStream()) ?: eventBytes.inputStream() - val event = serializer.deserializeEvent(inputStream.reader().buffered()) - event?.let { - events.add(event) + try { + val inputStream = config.encryption?.decrypt(eventBytes.inputStream()) ?: eventBytes.inputStream() + val event = serializer.deserializeEvent(inputStream.reader().buffered()) + event?.let { + events.add(event) + } + } catch (e: Throwable) { + iterator.remove() + config.logger.log("Event failed to parse: $e.") + } } - } - if (events.isNotEmpty()) { - api.batch(events) + if (events.isNotEmpty()) { + var deleteFiles = true + try { + api.batch(events) + } catch (e: PostHogApiError) { + if (e.statusCode >= 400) { + // TODO: the reason to delete or not the files? + } + throw e + } catch (e: IOException) { + // no connection should try again + if (e.isNetworkingError()) { + deleteFiles = false + } + throw e + } finally { + if (deleteFiles) { + legacyFile.delete() + } + } + } + } catch (e: Throwable) { + config.logger.log("Flushing legacy events failed: $e.") } - - legacyFile.delete() } } @@ -73,25 +99,45 @@ internal class SendCachedEventsIntegration(private val config: PostHogConfig, pr val time = startDate.time val fileFilter = FileFilter { file -> file.lastModified() <= time } - val listFiles = dir.listFiles(fileFilter) ?: emptyArray() + val listFiles = (dir.listFiles(fileFilter) ?: emptyArray()).toMutableList() val events = mutableListOf() val iterator = listFiles.iterator() while (iterator.hasNext()) { - val eventBytes = iterator.next() - - val inputStream = config.encryption?.decrypt(eventBytes.inputStream()) ?: eventBytes.inputStream() - val event = serializer.deserializeEvent(inputStream.reader().buffered()) - event?.let { - events.add(event) + val file = iterator.next() + + try { + val inputStream = config.encryption?.decrypt(file.inputStream()) ?: file.inputStream() + val event = serializer.deserializeEvent(inputStream.reader().buffered()) + event?.let { + events.add(event) + } + } catch (e: Throwable) { + iterator.remove() + file.delete() + config.logger.log("File: ${file.name} failed to parse: $e.") } } if (events.isNotEmpty()) { - api.batch(events) - - listFiles.forEach { file -> - file.delete() + var deleteFiles = true + try { + api.batch(events) + } catch (e: PostHogApiError) { + if (e.statusCode >= 400) { + // TODO: the reason to delete or not the files? + } + } catch (e: IOException) { + // no connection should try again + if (e.isNetworkingError()) { + deleteFiles = false + } + } finally { + if (deleteFiles) { + listFiles.forEach { file -> + file.delete() + } + } } } }