Skip to content

Commit f99b566

Browse files
authored
feat: [SDK-2197] Add LDReplay. registerActivity() method for React Native. (#472)
## Summary Adds a `LDReplay. registerActivity()` method that allows registering an activity after it has already been created. Normally, Android Session Replay uses Activity lifecycle hooks to listen for when Activities get created and register them. But React Native Session Replay gets initialized after the main Activity has already been created. So this method can be used to hook it up manually. ## How did you test this change? I manually tested this by publishing to a local maven repo and using it with the example app and a modified session replay SDK. ## Are there any deployment considerations? N/A <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk additive change that exposes a new optional API for wiring touch capture when initialization happens after an `Activity` has started; primary risk is incorrect registration leading to missed or duplicate touch interception in edge cases. > > **Overview** > Adds a new opt-in `LDReplay.registerActivity(Activity)` API to support frameworks (e.g. React Native) that initialize Session Replay after the host `Activity` is already running. > > This threads a new `registerActivity` call through `LDReplay` → `SessionReplayService` → `InteractionSource`, where `InteractionSource.registerActivity` simulates the `onActivityStarted`/`onActivityResumed` lifecycle events to install the touch interceptor and set the active window. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fe49982. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- ld-jira-link --> --- Related Jira issue: [SDK-2197: Fix Android RN Session Replay not attaching to MainActivity](https://launchdarkly.atlassian.net/browse/SDK-2197) <!-- end-ld-jira-link -->
1 parent a0f5cf8 commit f99b566

4 files changed

Lines changed: 49 additions & 0 deletions

File tree

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/InteractionSource.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ class InteractionSource(
153153
application.registerActivityLifecycleCallbacks(this)
154154
}
155155

156+
/**
157+
* Registers the given activity for touch capture, as if [onActivityStarted]
158+
* and [onActivityResumed] had already fired for it. Call this when the SDK is initialized
159+
* after the activity is already running (e.g. React Native).
160+
*/
161+
fun registerActivity(activity: Activity) {
162+
onActivityStarted(activity)
163+
onActivityResumed(activity)
164+
}
165+
156166
/**
157167
* Detaches the [InteractionSource] from the [Application].
158168
*/

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/SessionReplayService.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.launchdarkly.observability.replay
22

3+
import android.app.Activity
34
import androidx.lifecycle.DefaultLifecycleObserver
45
import androidx.lifecycle.Lifecycle
56
import androidx.lifecycle.LifecycleOwner
@@ -233,6 +234,14 @@ class SessionReplayService(
233234
}
234235
}
235236

237+
/**
238+
* Registers [activity] for touch capture. Call this after SDK initialization when the
239+
* activity is already running (e.g. React Native, where init happens after the activity starts).
240+
*/
241+
override fun registerActivity(activity: Activity) {
242+
interactionSource?.registerActivity(activity)
243+
}
244+
236245
// TODO: O11Y-621 - This should be called somewhere (Probably inside ObservabilityService.kt) to shutdown the instrumentation.
237246
fun shutdown() {
238247
pauseCapture()

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/sdk/LDReplay.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.launchdarkly.observability.sdk
22

3+
import android.app.Activity
34
import com.launchdarkly.observability.replay.plugin.SessionReplayHookProxy
45

56
/**
@@ -53,11 +54,23 @@ object LDReplay {
5354
fun flush() {
5455
delegate.flush()
5556
}
57+
58+
/**
59+
* Registers [activity] for touch capture.
60+
*
61+
* You do not normally need to call this. It is only necessary when the SDK is initialized
62+
* after the activity has already started (e.g. in React Native, where the host activity
63+
* is already running before the SDK initializes).
64+
*/
65+
fun registerActivity(activity: Activity) {
66+
delegate.registerActivity(activity)
67+
}
5668
}
5769

5870
internal interface SessionReplayServicing {
5971
fun start()
6072
fun stop()
6173
fun flush()
6274
fun afterIdentify(contextKeys: Map<String, String>, canonicalKey: String, completed: Boolean)
75+
fun registerActivity(activity: Activity) {}
6376
}

sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/sdk/LDReplayTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.launchdarkly.observability.sdk
22

3+
import android.app.Activity
4+
import io.mockk.mockk
35
import org.junit.jupiter.api.Assertions.assertEquals
46
import org.junit.jupiter.api.Test
57

@@ -9,6 +11,7 @@ class LDReplayTest {
911
var startCalls = 0
1012
var stopCalls = 0
1113
var flushCalls = 0
14+
var registerActivityCalls = 0
1215

1316
override fun start() {
1417
startCalls++
@@ -23,6 +26,10 @@ class LDReplayTest {
2326
}
2427

2528
override fun afterIdentify(contextKeys: Map<String, String>, canonicalKey: String, completed: Boolean) {}
29+
30+
override fun registerActivity(activity: Activity) {
31+
registerActivityCalls++
32+
}
2633
}
2734

2835
@Test
@@ -54,4 +61,14 @@ class LDReplayTest {
5461

5562
assertEquals(1, control.flushCalls)
5663
}
64+
65+
@Test
66+
fun `registerActivity delegates to replay control`() {
67+
val control = TestControl()
68+
LDReplay.init(control)
69+
70+
LDReplay.registerActivity(mockk<Activity>())
71+
72+
assertEquals(1, control.registerActivityCalls)
73+
}
5774
}

0 commit comments

Comments
 (0)