diff --git a/posthog-android/src/main/java/com/posthog/android/PostHogAndroid.kt b/posthog-android/src/main/java/com/posthog/android/PostHogAndroid.kt index 76e12dd2..c1e64711 100644 --- a/posthog-android/src/main/java/com/posthog/android/PostHogAndroid.kt +++ b/posthog-android/src/main/java/com/posthog/android/PostHogAndroid.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.Context import com.posthog.PostHog import com.posthog.PostHogInterface +import com.posthog.PostHogScreenProcessor import com.posthog.android.internal.MainHandler import com.posthog.android.internal.PostHogActivityLifecycleCallbackIntegration import com.posthog.android.internal.PostHogAndroidContext @@ -88,6 +89,10 @@ public class PostHogAndroid private constructor() { if (config.captureDeepLinks || config.captureScreenViews || config.sessionReplay) { config.addIntegration(PostHogActivityLifecycleCallbackIntegration(context, config)) } + // if the processor depends on captureScreenViews, only adds if its enabled + if (config.captureScreenViews) { + config.addProcessor(PostHogScreenProcessor()) + } } if (config.captureApplicationLifecycleEvents) { config.addIntegration(PostHogAppInstallIntegration(context, config)) diff --git a/posthog-android/src/main/java/com/posthog/android/internal/PostHogActivityLifecycleCallbackIntegration.kt b/posthog-android/src/main/java/com/posthog/android/internal/PostHogActivityLifecycleCallbackIntegration.kt index 9bf75dc0..350001d2 100644 --- a/posthog-android/src/main/java/com/posthog/android/internal/PostHogActivityLifecycleCallbackIntegration.kt +++ b/posthog-android/src/main/java/com/posthog/android/internal/PostHogActivityLifecycleCallbackIntegration.kt @@ -6,6 +6,7 @@ import android.app.Application.ActivityLifecycleCallbacks import android.os.Bundle import com.posthog.PostHog import com.posthog.PostHogIntegration +import com.posthog.ScreenTracker import com.posthog.android.PostHogAndroidConfig /** @@ -47,6 +48,7 @@ internal class PostHogActivityLifecycleCallbackIntegration( if (!screenName.isNullOrEmpty()) { PostHog.screen(screenName) + ScreenTracker.setCurrentScreen(screenName) } } } diff --git a/posthog/api/posthog.api b/posthog/api/posthog.api index bfe07b5c..836cdbfe 100644 --- a/posthog/api/posthog.api +++ b/posthog/api/posthog.api @@ -75,6 +75,7 @@ public class com/posthog/PostHogConfig { public fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;Lcom/posthog/PersonProfiles;)V public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZZZZIIIILcom/posthog/PostHogEncryption;Lcom/posthog/PostHogOnFeatureFlags;ZLcom/posthog/PostHogPropertiesSanitizer;Lkotlin/jvm/functions/Function1;Lcom/posthog/PersonProfiles;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addIntegration (Lcom/posthog/PostHogIntegration;)V + public final fun addProcessor (Lcom/posthog/PostHogPropertiesProcessor;)V public final fun getApiKey ()Ljava/lang/String; public final fun getCachePreferences ()Lcom/posthog/internal/PostHogPreferences; public final fun getContext ()Lcom/posthog/internal/PostHogContext; @@ -95,6 +96,7 @@ public class com/posthog/PostHogConfig { public final fun getOptOut ()Z public final fun getPersonProfiles ()Lcom/posthog/PersonProfiles; public final fun getPreloadFeatureFlags ()Z + public final fun getProcessors ()Ljava/util/List; public final fun getPropertiesSanitizer ()Lcom/posthog/PostHogPropertiesSanitizer; public final fun getReplayStoragePrefix ()Ljava/lang/String; public final fun getSdkName ()Ljava/lang/String; @@ -236,13 +238,28 @@ public abstract interface class com/posthog/PostHogOnFeatureFlags { public abstract fun loaded ()V } +public abstract interface class com/posthog/PostHogPropertiesProcessor { + public abstract fun process (Ljava/util/Map;)Ljava/util/Map; +} + public abstract interface class com/posthog/PostHogPropertiesSanitizer { public abstract fun sanitize (Ljava/util/Map;)Ljava/util/Map; } +public final class com/posthog/PostHogScreenProcessor : com/posthog/PostHogPropertiesProcessor { + public fun ()V + public fun process (Ljava/util/Map;)Ljava/util/Map; +} + public abstract interface annotation class com/posthog/PostHogVisibleForTesting : java/lang/annotation/Annotation { } +public final class com/posthog/ScreenTracker { + public static final field INSTANCE Lcom/posthog/ScreenTracker; + public final fun getCurrentScreenName ()Ljava/lang/String; + public final fun setCurrentScreen (Ljava/lang/String;)V +} + public abstract interface class com/posthog/internal/PostHogContext { public abstract fun getDynamicContext ()Ljava/util/Map; public abstract fun getSdkInfo ()Ljava/util/Map; diff --git a/posthog/src/main/java/com/posthog/PostHog.kt b/posthog/src/main/java/com/posthog/PostHog.kt index e4372300..03141f11 100644 --- a/posthog/src/main/java/com/posthog/PostHog.kt +++ b/posthog/src/main/java/com/posthog/PostHog.kt @@ -441,8 +441,14 @@ public class PostHog private constructor( appendGroups = !groupIdentify, ) + var currentProperties = mergedProperties + // has to run before the sanitizer so people can remove stuff processors add + config?.processors?.forEach { + currentProperties = it.process(currentProperties.toMutableMap()) + } + // sanitize the properties or fallback to the original properties - val sanitizedProperties = config?.propertiesSanitizer?.sanitize(mergedProperties.toMutableMap()) ?: mergedProperties + val sanitizedProperties = config?.propertiesSanitizer?.sanitize(currentProperties.toMutableMap()) ?: currentProperties val postHogEvent = PostHogEvent( diff --git a/posthog/src/main/java/com/posthog/PostHogConfig.kt b/posthog/src/main/java/com/posthog/PostHogConfig.kt index 7cae8f72..b045e71b 100644 --- a/posthog/src/main/java/com/posthog/PostHogConfig.kt +++ b/posthog/src/main/java/com/posthog/PostHogConfig.kt @@ -156,6 +156,9 @@ public open class PostHogConfig( private val integrationsList: MutableList = mutableListOf() private val integrationLock = Any() + private val processorsList: MutableList = mutableListOf() + private val processorsLock = Any() + /** * The integrations list */ @@ -188,6 +191,21 @@ public open class PostHogConfig( } } + public val processors: List + get() { + val list: List + synchronized(processorsLock) { + list = processorsList.toList() + } + return list + } + + public fun addProcessor(processor: PostHogPropertiesProcessor) { + synchronized(processorsLock) { + processorsList.add(processor) + } + } + public companion object { public const val DEFAULT_HOST: String = "https://us.i.posthog.com" } diff --git a/posthog/src/main/java/com/posthog/PostHogPropertiesProcessor.kt b/posthog/src/main/java/com/posthog/PostHogPropertiesProcessor.kt new file mode 100644 index 00000000..8a6c7970 --- /dev/null +++ b/posthog/src/main/java/com/posthog/PostHogPropertiesProcessor.kt @@ -0,0 +1,5 @@ +package com.posthog + +public fun interface PostHogPropertiesProcessor { + public fun process(properties: MutableMap): Map +} diff --git a/posthog/src/main/java/com/posthog/PostHogScreenProcessor.kt b/posthog/src/main/java/com/posthog/PostHogScreenProcessor.kt new file mode 100644 index 00000000..4b07d30b --- /dev/null +++ b/posthog/src/main/java/com/posthog/PostHogScreenProcessor.kt @@ -0,0 +1,15 @@ +package com.posthog + +public class PostHogScreenProcessor : PostHogPropertiesProcessor { + override fun process(properties: MutableMap): Map { + if (properties.containsKey("\$screen_name")) { + return properties + } + + val currentScreen = ScreenTracker.getCurrentScreenName() + + properties["\$screen_name"] = currentScreen + + return properties + } +} diff --git a/posthog/src/main/java/com/posthog/ScreenTracker.kt b/posthog/src/main/java/com/posthog/ScreenTracker.kt new file mode 100644 index 00000000..1ae4e8d8 --- /dev/null +++ b/posthog/src/main/java/com/posthog/ScreenTracker.kt @@ -0,0 +1,13 @@ +package com.posthog + +public object ScreenTracker { + @Volatile private lateinit var currentScreen: String + + public fun setCurrentScreen(screenName: String) { + currentScreen = screenName + } + + public fun getCurrentScreenName(): String { + return currentScreen + } +} diff --git a/posthog/src/test/java/com/posthog/PostHogTest.kt b/posthog/src/test/java/com/posthog/PostHogTest.kt index fbfd49e6..6722e219 100644 --- a/posthog/src/test/java/com/posthog/PostHogTest.kt +++ b/posthog/src/test/java/com/posthog/PostHogTest.kt @@ -45,6 +45,7 @@ internal class PostHogTest { reloadFeatureFlags: Boolean = true, sendFeatureFlagEvent: Boolean = true, integration: PostHogIntegration? = null, + processor: PostHogPropertiesProcessor? = null, cachePreferences: PostHogMemoryPreferences = PostHogMemoryPreferences(), propertiesSanitizer: PostHogPropertiesSanitizer? = null, ): PostHogInterface { @@ -58,6 +59,9 @@ internal class PostHogTest { if (integration != null) { addIntegration(integration) } + if (processor != null) { + addProcessor(processor) + } this.sendFeatureFlagEvent = sendFeatureFlagEvent this.cachePreferences = cachePreferences this.propertiesSanitizer = propertiesSanitizer @@ -1128,4 +1132,61 @@ internal class PostHogTest { sut.close() } + + @Test + fun `processor does not override existing screen_name in event properties`() { + val http = mockHttp() + val url = http.url("/") + + val sut = getSut(url.toString(), preloadFeatureFlags = false, reloadFeatureFlags = false, processor = PostHogScreenProcessor()) + + ScreenTracker.setCurrentScreen("CurrentScreen") + + sut.capture( + "Test Event", + properties = + mapOf( + "test_prop" to "test_value", + "\$screen_name" to "ProvidedScreen", + ), + ) + + queueExecutor.shutdownAndAwaitTermination() + + val request = http.takeRequest() + + val content = request.body.unGzip() + val batch = serializer.deserialize(content.reader()) + + val theEvent = batch.batch.first() + + assertEquals("ProvidedScreen", theEvent.properties?.get("\$screen_name")) + + sut.close() + } + + @Test + fun `processor adds screen_name to event properties`() { + val http = mockHttp() + val url = http.url("/") + + val sut = getSut(url.toString(), preloadFeatureFlags = false, reloadFeatureFlags = false, processor = PostHogScreenProcessor()) + + ScreenTracker.setCurrentScreen("TestScreen") + + sut.capture("Test Event", properties = mapOf("test_prop" to "test_value")) + + queueExecutor.shutdownAndAwaitTermination() + + val request = http.takeRequest() + + val content = request.body.unGzip() + val batch = serializer.deserialize(content.reader()) + + val theEvent = batch.batch.first() + + assertEquals("TestScreen", theEvent.properties?.get("\$screen_name")) + + sut.close() + } }