Skip to content

feat(session-replay-react-native): honor isEnabled: false and add initializeSessionReplay - #785

Merged
abelonogov-ld merged 5 commits into
mainfrom
fix/rn-session-replay-deferred-start
Sep 30, 2026
Merged

abelonogov-ld merged 5 commits into
mainfrom
fix/rn-session-replay-deferred-start

Conversation

@abelonogov-ld

@abelonogov-ld abelonogov-ld commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

The bug

The README documents deferred recording as isEnabled: false plus a later startSessionReplay(), but that never recorded. startSessionReplay() re-applied the configured isEnabled, so for a deferred start it wrote the false straight back:

  • iOS — LDReplay.shared.isEnabled = sessionReplayOptions.isEnabled
  • Android — applyEnabled(localReplayOptions.enabled), which calls LDReplay.stop() on false

The same code is the plugin's lazy-initialization path, which is why it read the config at all: one method was doing double duty as "initialize, honoring config" and as the public "start recording". Those wants are contradictory, so this splits them.

The fix

before after
plugin auto-start startSessionReplay() initializeSessionReplay() — initializes, honors isEnabled
public start startSessionReplay() — re-applied isEnabled startSessionReplay() — initializes if needed, then always enables

Neither adapter writes false any more; stopSessionReplay() is the only thing that stops recording. That also fixes a quieter bug: a re-init — a JS soft/OTA reload re-running plugin setup, for instance — used to silently stop a recording an earlier startSessionReplay() had begun, because the native instance outlives the JS context.

configureAndStart feature-detects initializeSessionReplay, since a JS-only OTA update can run this bundle against an older native binary. Falling back to startSessionReplay() there is safe: honoring isEnabled on the first start is exactly the old binary's behavior.

Both platforms now also report why a start didn't record, instead of always resolving as success. They call LDReplay.start(ignoreSampling: false) and map the result identically: started / alreadyStarted and sampledOut resolve — honoring sampleRate is not a failure — while unavailable and unrecoverableError reject. start is used over isEnabled = true because the setter's no-change guard would otherwise swallow a start on a session that is enabled but not recording.

Dependency bumps

Both are needed for start(ignoreSampling:) and its result enum:

  • iOS — LaunchDarklySessionReplay ~> 0.53.1 → ~> 0.56.0
  • Android — launchdarkly-observability-android 0.66.1 → 0.69.0, which carries #783

Verification

  • iOS, on device, against pod 0.56.0. A single configure, then both new paths firing as designed:
    [SR init] configure: ... enabled=false, sampleRate=1.0
    LaunchDarkly Session Replay started, version: 0.56.0
    [SR init] start: leaving recording off (isEnabled=false)   <- init honors the config
    [SR init] start: already initialized, forceEnable=true     <- explicit start overrides it
    
    Recording was confirmed positively, not by absence of errors: a control run with sampleRate: 0 logs Session Replay skipped by sampling, and that line is absent at sampleRate: 1.0.
  • JS — 21/21, including that the plugin calls initializeSessionReplay and not startSessionReplay, that isEnabled: false does not force recording on, and the OTA fallback.
  • Android — 10/10 against 0.69.0, including that both entry points reject a start that configure() never set up.

One coverage gap worth flagging: the Android result mapping sits after Handler.post, which the package's plain JVM unit tests cannot reach, so it is not covered here. The enum-to-behavior mapping itself is covered by the core SDK's tests in #783, and iOS has no adapter-level test for its equivalent either.

Other changes

  • README gains a "Delaying recording" section and an isEnabled row in the options table, and the sampleRate description no longer implies it only applies via isEnabled.
  • The example app now exercises the deferred path as a user would, replacing the configure-with-isEnabled: true workaround it needed before.

Note

Overview
Fixes deferred session replay: isEnabled: false plus a later startSessionReplay() used to leave recording off because the native start path re-applied the configured isEnabled (and could even stop an active recording on JS re-init).

The plugin now auto-starts via initializeSessionReplay(), which initializes native replay/observability and only turns on recording when isEnabled is true. startSessionReplay() still initializes if needed but always enables recording, without another configure call. Only stopSessionReplay() stops recording.

iOS and Android adapters take a forceEnable flag, stop toggling enabled off on start, and call LDReplay.start(ignoreSampling: false) with mapped outcomes (sampledOut succeeds; unavailable / unrecoverable errors reject). Native deps are bumped (LaunchDarklySessionReplay ~> 0.56.0, launchdarkly-observability-android 0.69.0). JS adds OTA fallback when initializeSessionReplay is missing, README/docs for delaying recording, example Start replay button, and expanded tests.

Reviewed by Cursor Bugbot for commit 6d3a1b8. Bugbot is set up for automated code reviews on this repo. Configure here.

abelonogov-ld and others added 2 commits September 29, 2026 17:04
… 0.56.0

Picks up LaunchDarkly iOS Client SDK 11.6.2. Changes since 0.53.1 are additive
apart from ObservabilityOptions.crashReporting now defaulting to .disabled,
which the adapter already sets explicitly.

Co-authored-by: Cursor <cursoragent@cursor.com>
…tializeSessionReplay

startSessionReplay() re-applied the configured isEnabled, so the documented
deferred-start pattern (isEnabled: false, then startSessionReplay() later) never
recorded — the call wrote false back on iOS and called LDReplay.stop() on Android.

startSessionReplay() now always enables recording, and the plugin's auto-start
moved to a new initializeSessionReplay() that honors isEnabled. Neither adapter
writes false any more, so a re-init (a JS soft/OTA reload, say) no longer silently
stops a recording an earlier start began. The plugin feature-detects the new
method so an OTA bundle running against an older native binary still initializes.

On iOS the outcome is mapped from LDReplay.start(ignoreSampling:), so an
unavailable plugin or an unrecoverable error rejects the promise instead of
resolving as success. Android keeps plain LDReplay.start() until the
launchdarkly-observability-android dependency exposes the same result API.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abelonogov-ld
abelonogov-ld requested a review from a team as a code owner September 30, 2026 00:19
…observability-android 0.69.0

Bumps launchdarkly-observability-android 0.66.1 -> 0.69.0 to pick up
LDReplay.start(ignoreSampling): SessionReplayStartResult, and maps the result the
same way iOS already does: started/alreadyStarted and sampledOut resolve, while
unavailable and unrecoverableError reject with the same messages. This clears the
TODO left in the previous commit, so both platforms now report why a start did
not record instead of always resolving as success.

The call runs inline on the main thread rather than hopping, because the adapter
is already inside a main-looper post and the core executor runs the block
directly when it is already on the main thread.

The mapping sits after Handler.post, which the plain JVM unit tests cannot reach;
the enum-to-behavior mapping is covered by the core SDK's own tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abelonogov-ld
abelonogov-ld enabled auto-merge (squash) September 30, 2026 21:22

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6d3a1b8. Configure here.

@abelonogov-ld
abelonogov-ld merged commit 9f9ca81 into main Sep 30, 2026
24 checks passed
@abelonogov-ld
abelonogov-ld deleted the fix/rn-session-replay-deferred-start branch September 30, 2026 21:46
abelonogov-ld pushed a commit that referenced this pull request Sep 30, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>session-replay-react-native: 0.24.0</summary>

##
[0.24.0](session-replay-react-native-0.23.0...session-replay-react-native-0.24.0)
(2026-09-30)


### Features

* **session-replay-react-native:** honor isEnabled: false and add
initializeSessionReplay
([#785](#785))
([9f9ca81](9f9ca81))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> **Release Please** publishes
`@launchdarkly/session-replay-react-native` **0.24.0** by bumping the
package version, updating `.release-please-manifest.json`, and adding a
**0.24.0** changelog entry.
> 
> The release notes document feature work from
[#785](#785)
(already on main): honoring **`isEnabled: false`** so replay does not
auto-start, and exposing **`initializeSessionReplay`** for explicit
initialization. This PR does not include additional source changes
beyond version and release metadata.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9867231. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants