Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changeset/calm-guards-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'posthog': patch
'posthog-android': patch
---

Prevent duplicate integration installation during concurrent SDK setup.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.os.Bundle
import com.posthog.PostHogIntegration
import com.posthog.PostHogInterface
import com.posthog.android.PostHogAndroidConfig
import java.util.concurrent.atomic.AtomicBoolean

/**
* Captures deep link and screen view events
Expand All @@ -18,10 +19,10 @@ internal class PostHogActivityLifecycleCallbackIntegration(
private val config: PostHogAndroidConfig,
) : ActivityLifecycleCallbacks, PostHogIntegration {
private var postHog: PostHogInterface? = null
private var ownsInstallation = false

private companion object {
@Volatile
private var integrationInstalled = false
private val integrationInstalled = AtomicBoolean(false)
}

override fun onActivityCreated(
Expand Down Expand Up @@ -83,19 +84,28 @@ internal class PostHogActivityLifecycleCallbackIntegration(
override fun onActivityDestroyed(activity: Activity) {
}

@Synchronized
override fun install(postHog: PostHogInterface) {
if (integrationInstalled) {
if (!integrationInstalled.compareAndSet(false, true)) {
return
Comment thread
marandaneto marked this conversation as resolved.
}
integrationInstalled = true
ownsInstallation = true

this.postHog = postHog
application.registerActivityLifecycleCallbacks(this)
}

@Synchronized
override fun uninstall() {
this.postHog = null
integrationInstalled = false
application.unregisterActivityLifecycleCallbacks(this)
if (!ownsInstallation) {
return
}
try {
this.postHog = null
application.unregisterActivityLifecycleCallbacks(this)
} finally {
ownsInstallation = false
integrationInstalled.set(false)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.posthog.PostHogInterface
import com.posthog.android.PostHogAndroidConfig
import com.posthog.internal.PostHogPreferences.Companion.BUILD
import com.posthog.internal.PostHogPreferences.Companion.VERSION
import java.util.concurrent.atomic.AtomicBoolean

/**
* Captures app installed and updated events
Expand All @@ -16,23 +17,25 @@ internal class PostHogAppInstallIntegration(
private val context: Context,
private val config: PostHogAndroidConfig,
) : PostHogIntegration {
private var ownsInstallation = false

private companion object {
@Volatile
private var integrationInstalled = false
private val integrationInstalled = AtomicBoolean(false)
}

@Synchronized
override fun install(postHog: PostHogInterface) {
if (integrationInstalled) {
return
}
// While the store is unreadable (Direct Boot) VERSION/BUILD read as absent, which would
// fire a spurious "Application Installed" for an existing install and overwrite the
// persisted previous build on unlock. Stay uninstalled so the correct event can still be
// emitted once the store is readable.
if (config.cachePreferences?.isAvailable() == false) {
return
}
integrationInstalled = true
if (!integrationInstalled.compareAndSet(false, true)) {
return
}
ownsInstallation = true

getPackageInfo(context, config)?.let { packageInfo ->
config.cachePreferences?.let { preferences ->
Expand Down Expand Up @@ -74,7 +77,12 @@ internal class PostHogAppInstallIntegration(
}
}

@Synchronized
override fun uninstall() {
integrationInstalled = false
if (!ownsInstallation) {
return
}
ownsInstallation = false
integrationInstalled.set(false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.posthog.android.PostHogAndroidConfig
import com.posthog.internal.PostHogSessionManager
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.atomic.AtomicBoolean

/**
* Captures app opened and backgrounded events
Expand All @@ -32,6 +33,7 @@ internal class PostHogLifecycleObserverIntegration(
private val bgEndSessionDelayMs = (1000 * 60 * 30).toLong() // 30 minutes

private var postHog: PostHogInterface? = null
private var ownsInstallation = false

private companion object {
// in case there are multiple instances or the SDK is closed/setup again
Expand All @@ -40,8 +42,7 @@ internal class PostHogLifecycleObserverIntegration(
@Volatile
private var fromBackground = false

@Volatile
private var integrationInstalled = false
private val integrationInstalled = AtomicBoolean(false)
}

override fun onStart(owner: LifecycleOwner) {
Expand Down Expand Up @@ -124,11 +125,12 @@ internal class PostHogLifecycleObserverIntegration(
lifecycle.addObserver(this)
}

@Synchronized
override fun install(postHog: PostHogInterface) {
if (integrationInstalled) {
if (!integrationInstalled.compareAndSet(false, true)) {
return
}
integrationInstalled = true
ownsInstallation = true

try {
this.postHog = postHog
Expand All @@ -148,9 +150,12 @@ internal class PostHogLifecycleObserverIntegration(
lifecycle.removeObserver(this)
}

@Synchronized
override fun uninstall() {
if (!ownsInstallation) {
return
}
try {
integrationInstalled = false
this.postHog = null
if (isMainThread(mainHandler)) {
remove()
Expand All @@ -161,6 +166,9 @@ internal class PostHogLifecycleObserverIntegration(
}
} catch (e: Throwable) {
config.logger.log("Failed to uninstall PostHogLifecycleObserverIntegration: $e")
} finally {
ownsInstallation = false
integrationInstalled.set(false)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import curtains.OnRootViewsChangedListener
import curtains.TouchEventInterceptor
import curtains.phoneWindow
import curtains.touchEventInterceptors
import java.util.concurrent.atomic.AtomicBoolean

/**
* Marks user touches as session activity by calling [PostHogSessionManager.touchSession]
Expand All @@ -22,9 +23,10 @@ import curtains.touchEventInterceptors
internal class PostHogTouchActivityIntegration(
private val config: PostHogAndroidConfig,
) : PostHogIntegration {
private var ownsInstallation = false

private companion object {
@Volatile
private var integrationInstalled = false
private val integrationInstalled = AtomicBoolean(false)
}

private val touchInterceptor =
Expand Down Expand Up @@ -53,11 +55,12 @@ internal class PostHogTouchActivityIntegration(
}
}

@Synchronized
override fun install(postHog: PostHogInterface) {
if (integrationInstalled || !isSupported()) {
if (!isSupported() || !integrationInstalled.compareAndSet(false, true)) {
return
}
integrationInstalled = true
ownsInstallation = true
try {
Curtains.rootViews.forEach { view ->
view.phoneWindow?.let { window ->
Expand All @@ -72,7 +75,11 @@ internal class PostHogTouchActivityIntegration(
}
}

@Synchronized
override fun uninstall() {
if (!ownsInstallation) {
return
}
try {
Curtains.onRootViewsChangedListeners -= onRootViewsChangedListener
Curtains.rootViews.forEach { view ->
Expand All @@ -83,7 +90,8 @@ internal class PostHogTouchActivityIntegration(
} catch (e: Throwable) {
config.logger.log("PostHogTouchActivityIntegration uninstall failed: $e.")
} finally {
integrationInstalled = false
ownsInstallation = false
integrationInstalled.set(false)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean

public class PostHogReplayIntegration(
private val context: Context,
Expand Down Expand Up @@ -191,6 +192,7 @@ public class PostHogReplayIntegration(

private var postHog: PostHogInterface? = null
private var replayQueue: PostHogReplayQueue? = null
private var ownsInstallation = false

@Volatile
private var replaySessionId: String? = null
Expand Down Expand Up @@ -524,11 +526,12 @@ public class PostHogReplayIntegration(
decorViews.remove(view)
}

@Synchronized
override fun install(postHog: PostHogInterface) {
if (integrationInstalled || !isSupported()) {
if (!isSupported() || !integrationInstalled.compareAndSet(false, true)) {
return
}
integrationInstalled = true
ownsInstallation = true
this.postHog = postHog

// Wire up as buffer delegate for the replay queue
Expand Down Expand Up @@ -558,9 +561,12 @@ public class PostHogReplayIntegration(
}
}

@Synchronized
override fun uninstall() {
if (!ownsInstallation) {
return
}
try {
integrationInstalled = false
this.postHog = null

// Clear buffer delegate
Expand Down Expand Up @@ -589,6 +595,9 @@ public class PostHogReplayIntegration(
decorViews.clear()
} catch (e: Throwable) {
config.logger.log("Session Replay uninstall failed: $e.")
} finally {
ownsInstallation = false
integrationInstalled.set(false)
}
}

Expand Down Expand Up @@ -2335,7 +2344,6 @@ public class PostHogReplayIntegration(
const val ANDROID_COMPOSE_VIEW_CLASS_NAME: String = "androidx.compose.ui.platform.AndroidComposeView"
const val ANDROID_COMPOSE_VIEW: String = "AndroidComposeView"

@Volatile
private var integrationInstalled = false
private val integrationInstalled = AtomicBoolean(false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.posthog.internal.replay.RRPluginEvent
import com.posthog.internal.replay.capture
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean

internal class PostHogLogCatIntegration(private val config: PostHogAndroidConfig) : PostHogIntegration {
@Volatile
Expand All @@ -20,22 +21,23 @@ internal class PostHogLogCatIntegration(private val config: PostHogAndroidConfig
get() = postHog?.isSessionReplayActive() ?: false

private var postHog: PostHogInterface? = null
private var ownsInstallation = false

private companion object {
@Volatile
private var integrationInstalled = false
private val integrationInstalled = AtomicBoolean(false)
}

@Synchronized
override fun install(postHog: PostHogInterface) {
this.postHog = postHog
if (integrationInstalled) {
return
}
val captureLogcat = config.remoteConfigHolder?.isConsoleLogRecordingEnabled() ?: true
if (!config.sessionReplayConfig.captureLogcat || !captureLogcat) {
return
}
integrationInstalled = true
if (!integrationInstalled.compareAndSet(false, true)) {
return
}
ownsInstallation = true
val cmd = mutableListOf("logcat", "-v", "threadtime", "*:E")
val sdf = SimpleDateFormat("MM-dd HH:mm:ss.mmm", Locale.ROOT)
cmd.add("-T")
Expand Down Expand Up @@ -107,11 +109,19 @@ internal class PostHogLogCatIntegration(private val config: PostHogAndroidConfig
}

@PostHogVisibleForTesting
internal fun isInstalled(): Boolean = integrationInstalled
internal fun isInstalled(): Boolean = integrationInstalled.get()

@Synchronized
override fun uninstall() {
integrationInstalled = false
logcatInProgress = false
logcatThread?.interruptSafely()
if (!ownsInstallation) {
return
}
try {
logcatInProgress = false
logcatThread?.interruptSafely()
} finally {
ownsInstallation = false
integrationInstalled.set(false)
}
}
}
Loading
Loading