Skip to content

Commit b6ec2b1

Browse files
authored
fix(react-native): propagate afterIdentify propagation in iOS session replay (#501)
## Summary Propagate afterIdentify to session replay in iOS the same as Android. ## How did you test this change? Manually ran the example app and verified the session replay shows up as desired. ## Are there any deployment considerations? N/A <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches iOS session replay initialization and context propagation; incorrect context building or ordering could affect user attribution in replays, but scope is limited to the adapter layer and adds defensive error handling. > > **Overview** > Aligns iOS with Android by wiring `afterIdentify` through the React Native session replay bridge to the native SDK hook, and caching the latest identified `LDContext`. > > `SessionReplayClientAdapter` now builds single- and multi-kind contexts from the `[kind: key]` map, updates `cachedContext` on successful identify, and uses that cached context when first starting `LDClient`. > > The iOS native module wraps `afterIdentify` in `@try/@catch` (matching other methods) and the example app’s `Podfile.lock` is updated to `SessionReplayReactNative` `0.5.0` (and CocoaPods `1.16.2`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2d22972. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 96c4516 commit b6ec2b1

3 files changed

Lines changed: 67 additions & 14 deletions

File tree

sdk/@launchdarkly/react-native-ld-session-replay/example/ios/Podfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2512,7 +2512,7 @@ PODS:
25122512
- React-perflogger (= 0.83.0)
25132513
- React-utils (= 0.83.0)
25142514
- SocketRocket
2515-
- SessionReplayReactNative (0.3.0):
2515+
- SessionReplayReactNative (0.5.0):
25162516
- boost
25172517
- DoubleConversion
25182518
- fast_float
@@ -2882,11 +2882,11 @@ SPEC CHECKSUMS:
28822882
ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568
28832883
ReactCodegen: 11c08ff43a62009d48c71de000352e4515918801
28842884
ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f
2885-
SessionReplayReactNative: 899a1416f5e99dd765070c83ef78f17dbbdf07e0
2885+
SessionReplayReactNative: 60664a81e02ef6a64da725c84fe6e709c6f0cd5d
28862886
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
28872887
SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
28882888
Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e
28892889

28902890
PODFILE CHECKSUM: 53fee6f649d87604b0c5ad827154b6c454d1a29a
28912891

2892-
COCOAPODS: 1.15.2
2892+
COCOAPODS: 1.16.2

sdk/@launchdarkly/react-native-ld-session-replay/ios/SessionReplayClientAdapter.swift

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public class SessionReplayClientAdapter: NSObject {
1515
private var lastTask: Task<Void, Never> = Task {}
1616

1717
@MainActor private var initialized = false
18+
// The most recently identified LDContext. Defaults to nil (LDClient will use its own anonymous
19+
// context). Updated on each successful identify via afterIdentify.
20+
@MainActor private var cachedContext: LDContext? = nil
1821

1922
private override init() {
2023
super.init()
@@ -52,17 +55,33 @@ public class SessionReplayClientAdapter: NSObject {
5255
return config
5356
}
5457

55-
private func makeContext() -> LDContext? {
56-
var contextBuilder = LDContextBuilder(
57-
key: "12345"
58-
)
59-
contextBuilder.kind("user")
60-
do {
61-
return try contextBuilder.build().get()
62-
} catch {
63-
NSLog("[SessionReplayAdapter] Failed to build LDContext: %@", error.localizedDescription)
58+
// Builds an LDContext from a [kind: key] map. Returns nil if the map is empty or a context
59+
// cannot be built. Mirrors buildContextFromKeys() in SessionReplayClientAdapter.kt.
60+
private func buildContextFromKeys(_ keys: [String: String]) -> LDContext? {
61+
guard let first = keys.first else { return nil }
62+
if keys.count == 1 {
63+
let (kind, key) = first
64+
var builder = LDContextBuilder(key: key)
65+
builder.kind(kind)
66+
guard case .success(let context) = builder.build() else {
67+
NSLog("[SessionReplayAdapter] Failed to build LDContext for kind=%@", kind)
68+
return nil
69+
}
70+
return context
71+
}
72+
var multiBuilder = LDMultiContextBuilder()
73+
for (kind, key) in keys {
74+
var builder = LDContextBuilder(key: key)
75+
builder.kind(kind)
76+
if case .success(let context) = builder.build() {
77+
multiBuilder.addContext(context)
78+
}
79+
}
80+
guard case .success(let context) = multiBuilder.build() else {
81+
NSLog("[SessionReplayAdapter] Failed to build multi-context")
6482
return nil
6583
}
84+
return context
6685
}
6786

6887
@objc public func start(completion: @escaping (Bool, String?) -> Void) {
@@ -78,7 +97,7 @@ public class SessionReplayClientAdapter: NSObject {
7897
guard let self else { return }
7998
if !self.initialized {
8099
let config = self.makeConfig(mobileKey: mobileKey, options: sessionReplayOptions)
81-
let context = self.makeContext()
100+
let context = self.cachedContext
82101
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
83102
LDClient.start(config: config, context: context, startWaitSeconds: 0) { _ in
84103
cont.resume()
@@ -96,6 +115,34 @@ public class SessionReplayClientAdapter: NSObject {
96115
}
97116
}
98117

118+
@objc public func afterIdentify(contextKeys: NSDictionary, canonicalKey: String, completed: Bool) {
119+
var keys = [String: String]()
120+
for (k, v) in contextKeys {
121+
if let kind = k as? String, let key = v as? String {
122+
keys[kind] = key
123+
}
124+
}
125+
lock.lock()
126+
defer { lock.unlock() }
127+
let prev = lastTask
128+
lastTask = Task { @MainActor [weak self] in
129+
await prev.value
130+
guard let self else { return }
131+
if completed {
132+
// If buildContextFromKeys returns nil, that's fine — LaunchDarkly will
133+
// use a default anonymous context.
134+
self.cachedContext = self.buildContextFromKeys(keys)
135+
}
136+
if self.initialized {
137+
LDReplay.shared.hookProxy?.afterIdentify(
138+
contextKeys: contextKeys,
139+
canonicalKey: canonicalKey,
140+
completed: completed
141+
)
142+
}
143+
}
144+
}
145+
99146
/// There is almost no reason to stop the LDClient. Normally, set the LDClient offline to stop communication with the LaunchDarkly servers. Stop the LDClient to stop recording events. There is no need to stop the LDClient prior to suspending, moving to the background, or terminating the app. The SDK will respond to these events as the system requires and as configured in LDConfig.
100147
///
101148
/// So in order to not record anything from the Swift's LDClient, LDClient is configured to be offline in the start method

sdk/@launchdarkly/react-native-ld-session-replay/ios/SessionReplayReactNative.mm

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,13 @@ - (void)afterIdentify:(NSDictionary *)contextKeys
5959
resolve:(RCTPromiseResolveBlock)resolve
6060
reject:(RCTPromiseRejectBlock)reject
6161
{
62-
resolve(nil);
62+
@try {
63+
[[SessionReplayClientAdapter shared] afterIdentifyWithContextKeys:contextKeys canonicalKey:canonicalKey completed:completed];
64+
resolve(nil);
65+
} @catch(NSException *exception) {
66+
NSLog(@"⚠️ afterIdentify crash: %@", exception);
67+
reject(@"after_identify_failed", exception.reason, nil);
68+
}
6369
}
6470

6571
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:

0 commit comments

Comments
 (0)