@mixpanel/react-native-session-replay 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MixpanelReactNativeSessionReplay.podspec +1 -1
- package/README.md +1 -1
- package/android/build.gradle +10 -3
- package/android/src/main/java/com/mixpanelreactnativesessionreplay/MixpanelReactNativeSessionReplayModule.kt +54 -0
- package/android/src/main/java/com/mixpanelreactnativesessionreplay/MixpanelSessionReplayView.kt +56 -0
- package/android/src/main/java/com/mixpanelreactnativesessionreplay/MixpanelSessionReplayViewManager.kt +5 -0
- package/android/src/test/java/com/mixpanelreactnativesessionreplay/WireframeTextDeclarationTest.kt +41 -0
- package/ios/MixpanelReactNativeSessionReplay.h +7 -2
- package/ios/MixpanelReactNativeSessionReplay.mm +35 -2
- package/ios/MixpanelSessionReplayViewManager.mm +9 -0
- package/ios/MixpanelSwiftSessionReplay.swift +43 -1
- package/lib/module/MixpanelSessionReplayView.js.map +1 -1
- package/lib/module/NativeMixpanelReactNativeSessionReplay.js.map +1 -1
- package/lib/module/index.js +468 -23
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/MixpanelSessionReplayView.d.ts +32 -0
- package/lib/typescript/src/MixpanelSessionReplayView.d.ts.map +1 -1
- package/lib/typescript/src/NativeMixpanelReactNativeSessionReplay.d.ts +2 -0
- package/lib/typescript/src/NativeMixpanelReactNativeSessionReplay.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +259 -10
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/MixpanelSessionReplayView.tsx +33 -0
- package/src/NativeMixpanelReactNativeSessionReplay.ts +7 -0
- package/src/index.tsx +515 -22
|
@@ -15,7 +15,7 @@ Pod::Spec.new do |s|
|
|
|
15
15
|
|
|
16
16
|
s.source_files = "ios/**/*.{h,m,mm,cpp,swift}"
|
|
17
17
|
s.private_header_files = "ios/**/*.h"
|
|
18
|
-
s.dependency 'MixpanelSessionReplay', '1.
|
|
18
|
+
s.dependency 'MixpanelSessionReplay', '1.6.1'
|
|
19
19
|
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
|
20
20
|
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
|
|
21
21
|
if respond_to?(:install_modules_dependencies, true)
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Mixpanel React Native Session Replay
|
|
2
2
|
|
|
3
|
-
#####
|
|
3
|
+
##### _October 08, 2026_ - [v1.4.1](https://github.com/mixpanel/mixpanel-react-native-session-replay/releases/tag/v1.4.1)
|
|
4
4
|
|
|
5
5
|
Official React Native turbo module for Mixpanel Session Replay. Capture and analyze user interactions in your mobile app by bridging Mixpanel's native iOS and Android session replay SDKs to React Native.
|
|
6
6
|
|
package/android/build.gradle
CHANGED
|
@@ -17,7 +17,13 @@ buildscript {
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
apply plugin: "com.android.library"
|
|
20
|
-
|
|
20
|
+
// AGP 9 ships built-in Kotlin support and registers the `kotlin` extension
|
|
21
|
+
// itself. Applying the Kotlin plugin on top of it fails configuration with
|
|
22
|
+
// "Cannot add extension with name 'kotlin'". Only apply it when nothing has
|
|
23
|
+
// registered that extension yet.
|
|
24
|
+
if (project.extensions.findByName('kotlin') == null) {
|
|
25
|
+
apply plugin: "kotlin-android"
|
|
26
|
+
}
|
|
21
27
|
|
|
22
28
|
apply plugin: "com.facebook.react"
|
|
23
29
|
|
|
@@ -78,8 +84,9 @@ dependencies {
|
|
|
78
84
|
// Mixpanel Android SDK dependency (minimum v8.0.2)
|
|
79
85
|
implementation "com.mixpanel.android:mixpanel-android:8.0.2"
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
implementation "com.mixpanel.android:mixpanel-android-session-replay:1.5.0"
|
|
88
|
+
|
|
89
|
+
testImplementation "junit:junit:4.13.2"
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
react {
|
|
@@ -7,9 +7,11 @@ import com.facebook.react.module.annotations.ReactModule
|
|
|
7
7
|
import com.mixpanel.android.sessionreplay.MPSessionReplay
|
|
8
8
|
import com.mixpanel.android.sessionreplay.MPSessionReplayInstance
|
|
9
9
|
import com.mixpanel.android.sessionreplay.MPSessionReplayError
|
|
10
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
10
11
|
import com.mixpanel.android.sessionreplay.models.MPSessionReplayConfig
|
|
11
12
|
import com.mixpanel.android.sessionreplay.sensitive_views.AutoMaskedView
|
|
12
13
|
import com.mixpanel.android.sessionreplay.utils.APIConstants
|
|
14
|
+
import com.mixpanel.android.sessionreplay.wireframe.WireframeDebugSnapshot
|
|
13
15
|
import org.json.JSONObject
|
|
14
16
|
import org.json.JSONArray
|
|
15
17
|
|
|
@@ -52,6 +54,18 @@ class MixpanelReactNativeSessionReplayModule(reactContext: ReactApplicationConte
|
|
|
52
54
|
}
|
|
53
55
|
println("Mixpanel - Config variable fromJson: $replayConfig")
|
|
54
56
|
|
|
57
|
+
// `emitWireframes` is this bridge's own debug switch, not SDK configuration: the SDK's
|
|
58
|
+
// `DebugOptions.wireframeEmitter` is a callback, which cannot cross as JSON, so JS sends a
|
|
59
|
+
// boolean alongside it and the bridge attaches the destination the config could not carry.
|
|
60
|
+
// The SDK deliberately does not model the flag, so read it from the payload.
|
|
61
|
+
val emitWireframes =
|
|
62
|
+
config.optJSONObject("debugOptions")?.optBoolean("emitWireframes") == true
|
|
63
|
+
if (emitWireframes) {
|
|
64
|
+
replayConfig.debugOptions?.let { debugOptions ->
|
|
65
|
+
replayConfig.debugOptions = debugOptions.copy(wireframeEmitter = ::emitWireframeSnapshot)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
// Set library version and name
|
|
56
70
|
APIConstants.setLibVersion(LIB_VERSION)
|
|
57
71
|
APIConstants.setMpLib(MP_LIB)
|
|
@@ -70,6 +84,9 @@ class MixpanelReactNativeSessionReplayModule(reactContext: ReactApplicationConte
|
|
|
70
84
|
try {
|
|
71
85
|
// Configure AutoMaskedView categories with comprehensive React Native support
|
|
72
86
|
configureSensitiveClasses(instance, replayConfig)
|
|
87
|
+
// Android clears its native declaration registry during reinitialization. React
|
|
88
|
+
// does not resend unchanged props, so restore declarations cached by mounted views.
|
|
89
|
+
WireframeTextDeclarationRegistry.reapplyAll()
|
|
73
90
|
println("Mixpanel - Session Replay initialized successfully with enhanced view masking")
|
|
74
91
|
promise.resolve(null)
|
|
75
92
|
} catch (e: Exception) {
|
|
@@ -176,6 +193,40 @@ class MixpanelReactNativeSessionReplayModule(reactContext: ReactApplicationConte
|
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Forwards one captured wireframe to JavaScript as JSON.
|
|
198
|
+
*
|
|
199
|
+
* The snapshot crosses as a string rather than a `WritableMap` because
|
|
200
|
+
* [WireframeDebugSnapshot.toJson] already exists and is the same shape iOS sends —
|
|
201
|
+
* building a map here would make the bridge a third place the shape could drift.
|
|
202
|
+
*
|
|
203
|
+
* Called on the SDK's debug scope, off the main thread, once per captured frame.
|
|
204
|
+
* Delivery is best-effort: during teardown or a reload there is no JS runtime to emit
|
|
205
|
+
* into, and a debug aid must never take down the host app, so failures are logged and
|
|
206
|
+
* dropped.
|
|
207
|
+
*/
|
|
208
|
+
private fun emitWireframeSnapshot(snapshot: WireframeDebugSnapshot) {
|
|
209
|
+
try {
|
|
210
|
+
if (!reactApplicationContext.hasActiveReactInstance()) return
|
|
211
|
+
reactApplicationContext
|
|
212
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
213
|
+
.emit(WIREFRAME_EVENT, snapshot.toJson())
|
|
214
|
+
} catch (e: Exception) {
|
|
215
|
+
println("Mixpanel - Failed to deliver wireframe snapshot to JS: ${e.message}")
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Required by `NativeEventEmitter` on a TurboModule. No-ops here: events go out over
|
|
221
|
+
* [DeviceEventManagerModule.RCTDeviceEventEmitter], which broadcasts without per-listener
|
|
222
|
+
* registration. iOS inherits real implementations from `RCTEventEmitter`.
|
|
223
|
+
*/
|
|
224
|
+
@ReactMethod
|
|
225
|
+
override fun addListener(eventName: String) = Unit
|
|
226
|
+
|
|
227
|
+
@ReactMethod
|
|
228
|
+
override fun removeListeners(count: Double) = Unit
|
|
229
|
+
|
|
179
230
|
/**
|
|
180
231
|
* Safely configure sensitive classes for comprehensive React Native view masking
|
|
181
232
|
* Supports both old and new React Native architectures with crash prevention
|
|
@@ -309,5 +360,8 @@ class MixpanelReactNativeSessionReplayModule(reactContext: ReactApplicationConte
|
|
|
309
360
|
const val NAME = "MixpanelReactNativeSessionReplay"
|
|
310
361
|
const val LIB_VERSION = "1.2.0"
|
|
311
362
|
const val MP_LIB = "react-native-sr"
|
|
363
|
+
|
|
364
|
+
/** Must match `WIREFRAME_EVENT` in `src/index.tsx` and `supportedEvents` on iOS. */
|
|
365
|
+
const val WIREFRAME_EVENT = "MixpanelSessionReplayWireframe"
|
|
312
366
|
}
|
|
313
367
|
}
|
package/android/src/main/java/com/mixpanelreactnativesessionreplay/MixpanelSessionReplayView.kt
CHANGED
|
@@ -3,9 +3,53 @@ package com.mixpanelreactnativesessionreplay
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import com.facebook.react.views.view.ReactViewGroup
|
|
5
5
|
import com.mixpanel.android.sessionreplay.MPSessionReplay
|
|
6
|
+
import com.mixpanel.android.sessionreplay.extensions.mpWireframeText
|
|
7
|
+
import java.util.Collections
|
|
8
|
+
import java.util.WeakHashMap
|
|
9
|
+
|
|
10
|
+
internal class WireframeTextDeclaration(
|
|
11
|
+
private val applyText: (String?) -> Unit,
|
|
12
|
+
) {
|
|
13
|
+
private var text: String? = null
|
|
14
|
+
|
|
15
|
+
init {
|
|
16
|
+
WireframeTextDeclarationRegistry.register(this)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fun update(text: String?) {
|
|
20
|
+
if (this.text == text) return
|
|
21
|
+
|
|
22
|
+
this.text = text
|
|
23
|
+
applyText(text)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fun reapply() {
|
|
27
|
+
// A null value means the declaration is already absent from the freshly reset SDK registry.
|
|
28
|
+
text?.let(applyText)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
internal object WireframeTextDeclarationRegistry {
|
|
33
|
+
private val declarations =
|
|
34
|
+
Collections.synchronizedMap(WeakHashMap<WireframeTextDeclaration, Unit>())
|
|
35
|
+
|
|
36
|
+
fun register(declaration: WireframeTextDeclaration) {
|
|
37
|
+
declarations[declaration] = Unit
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fun reapplyAll() {
|
|
41
|
+
val snapshot = synchronized(declarations) { declarations.keys.toList() }
|
|
42
|
+
snapshot.forEach(WireframeTextDeclaration::reapply)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
internal fun clearForTests() {
|
|
46
|
+
declarations.clear()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
6
49
|
|
|
7
50
|
class MixpanelSessionReplayView(context: Context) : ReactViewGroup(context) {
|
|
8
51
|
private var isSensitive: Boolean = false
|
|
52
|
+
private val wireframeTextDeclaration = WireframeTextDeclaration(::mpWireframeText)
|
|
9
53
|
|
|
10
54
|
fun setSensitive(sensitive: Boolean) {
|
|
11
55
|
if (isSensitive == sensitive) return
|
|
@@ -13,6 +57,18 @@ class MixpanelSessionReplayView(context: Context) : ReactViewGroup(context) {
|
|
|
13
57
|
isSensitive = sensitive
|
|
14
58
|
updateViewSensitivity()
|
|
15
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Declares the text recorded for this view in the `mp_wireframe` event.
|
|
63
|
+
*
|
|
64
|
+
* Orthogonal to [setSensitive] — this has no bearing on which pixels are captured,
|
|
65
|
+
* and masking has no bearing on the declared text. Unlike sensitivity, it needs no
|
|
66
|
+
* live [MPSessionReplay] instance: the declaration is held by the SDK's view manager,
|
|
67
|
+
* so a prop that arrives before `initialize` still applies.
|
|
68
|
+
*/
|
|
69
|
+
fun setWireframeText(text: String?) {
|
|
70
|
+
wireframeTextDeclaration.update(text)
|
|
71
|
+
}
|
|
16
72
|
|
|
17
73
|
private fun updateViewSensitivity() {
|
|
18
74
|
MPSessionReplay.getInstance()?.let { sessionReplay ->
|
|
@@ -26,4 +26,9 @@ class MixpanelSessionReplayViewManager : ViewGroupManager<MixpanelSessionReplayV
|
|
|
26
26
|
println("MixpanelSessionReplayView - setSensitive called with value: $sensitive for view: $view")
|
|
27
27
|
view.setSensitive(sensitive)
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
@ReactProp(name = "wireframeText")
|
|
31
|
+
fun setWireframeText(view: MixpanelSessionReplayView, wireframeText: String?) {
|
|
32
|
+
view.setWireframeText(wireframeText)
|
|
33
|
+
}
|
|
29
34
|
}
|
package/android/src/test/java/com/mixpanelreactnativesessionreplay/WireframeTextDeclarationTest.kt
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package com.mixpanelreactnativesessionreplay
|
|
2
|
+
|
|
3
|
+
import org.junit.After
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Before
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
class WireframeTextDeclarationTest {
|
|
9
|
+
@Before
|
|
10
|
+
fun setUp() {
|
|
11
|
+
WireframeTextDeclarationRegistry.clearForTests()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@After
|
|
15
|
+
fun tearDown() {
|
|
16
|
+
WireframeTextDeclarationRegistry.clearForTests()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Test
|
|
20
|
+
fun `reapplies unchanged text after native registry reset`() {
|
|
21
|
+
val appliedValues = mutableListOf<String?>()
|
|
22
|
+
val declaration = WireframeTextDeclaration(appliedValues::add)
|
|
23
|
+
|
|
24
|
+
declaration.update("Checkout")
|
|
25
|
+
WireframeTextDeclarationRegistry.reapplyAll()
|
|
26
|
+
|
|
27
|
+
assertEquals(listOf("Checkout", "Checkout"), appliedValues)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test
|
|
31
|
+
fun `does not reapply a cleared declaration`() {
|
|
32
|
+
val appliedValues = mutableListOf<String?>()
|
|
33
|
+
val declaration = WireframeTextDeclaration(appliedValues::add)
|
|
34
|
+
|
|
35
|
+
declaration.update("Checkout")
|
|
36
|
+
declaration.update(null)
|
|
37
|
+
WireframeTextDeclarationRegistry.reapplyAll()
|
|
38
|
+
|
|
39
|
+
assertEquals(listOf("Checkout", null), appliedValues)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
#import <React/RCTEventEmitter.h>
|
|
2
|
+
|
|
1
3
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
2
4
|
#import <MixpanelReactNativeSessionReplaySpec/MixpanelReactNativeSessionReplaySpec.h>
|
|
3
5
|
#else
|
|
4
6
|
#import <React/RCTBridgeModule.h>
|
|
5
7
|
#endif
|
|
6
8
|
|
|
9
|
+
// Subclasses `RCTEventEmitter` for the wireframe debug channel. That also supplies the
|
|
10
|
+
// `addListener:`/`removeListeners:` the TurboModule spec now declares, so the protocol
|
|
11
|
+
// conformance below is satisfied without implementing them here.
|
|
7
12
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
8
|
-
@interface MixpanelReactNativeSessionReplay :
|
|
13
|
+
@interface MixpanelReactNativeSessionReplay : RCTEventEmitter <NativeMixpanelReactNativeSessionReplaySpec>
|
|
9
14
|
#else
|
|
10
|
-
@interface MixpanelReactNativeSessionReplay :
|
|
15
|
+
@interface MixpanelReactNativeSessionReplay : RCTEventEmitter <RCTBridgeModule>
|
|
11
16
|
#endif
|
|
12
17
|
|
|
13
18
|
@end
|
|
@@ -5,9 +5,33 @@
|
|
|
5
5
|
#import <MixpanelReactNativeSessionReplay/MixpanelReactNativeSessionReplay-Swift.h>
|
|
6
6
|
#endif
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/// Must match `WIREFRAME_EVENT` in `src/index.tsx` and `WIREFRAME_EVENT` on Android.
|
|
9
|
+
static NSString *const kMixpanelWireframeEvent = @"MixpanelSessionReplayWireframe";
|
|
10
|
+
|
|
11
|
+
@implementation MixpanelReactNativeSessionReplay {
|
|
12
|
+
// Set between `startObserving` and `stopObserving`. `sendEventWithName:` logs a warning
|
|
13
|
+
// when nothing is listening, and wireframes fire once per captured frame, so an app that
|
|
14
|
+
// enabled the emitter and then removed its listener would otherwise flood the console.
|
|
15
|
+
BOOL _hasWireframeListeners;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
RCT_EXPORT_MODULE()
|
|
10
19
|
|
|
20
|
+
- (NSArray<NSString *> *)supportedEvents
|
|
21
|
+
{
|
|
22
|
+
return @[ kMixpanelWireframeEvent ];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
- (void)startObserving
|
|
26
|
+
{
|
|
27
|
+
_hasWireframeListeners = YES;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
- (void)stopObserving
|
|
31
|
+
{
|
|
32
|
+
_hasWireframeListeners = NO;
|
|
33
|
+
}
|
|
34
|
+
|
|
11
35
|
RCT_EXPORT_METHOD(initialize:(nonnull NSString *)token distinctId:(nonnull NSString *)distinctId configJSON:(nonnull NSString *)configJSON resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject)
|
|
12
36
|
{
|
|
13
37
|
@try {
|
|
@@ -15,7 +39,16 @@ RCT_EXPORT_METHOD(initialize:(nonnull NSString *)token distinctId:(nonnull NSStr
|
|
|
15
39
|
reject(@"INVALID_CONFIG", @"Token and distinctId are required", nil);
|
|
16
40
|
return;
|
|
17
41
|
}
|
|
18
|
-
|
|
42
|
+
// Weak, because the SDK holds this block for the process lifetime — the module must not
|
|
43
|
+
// be kept alive by it, and after a reload there may be no module to deliver to.
|
|
44
|
+
__weak MixpanelReactNativeSessionReplay *weakSelf = self;
|
|
45
|
+
[MixpanelSwiftSessionReplay initialize:token distinctId:distinctId configJSON:configJSON wireframeHandler:^(NSString *snapshotJSON) {
|
|
46
|
+
MixpanelReactNativeSessionReplay *strongSelf = weakSelf;
|
|
47
|
+
if (strongSelf == nil || !strongSelf->_hasWireframeListeners) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
[strongSelf sendEventWithName:kMixpanelWireframeEvent body:snapshotJSON];
|
|
51
|
+
} completion:^(BOOL success, NSError * _Nullable error) {
|
|
19
52
|
if (success) {
|
|
20
53
|
resolve(nil);
|
|
21
54
|
} else {
|
|
@@ -33,4 +33,13 @@ RCT_CUSTOM_VIEW_PROPERTY(sensitive, BOOL, RCTView)
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Declares the text recorded for this view in the wireframe. Orthogonal to `sensitive`:
|
|
37
|
+
// declared text is authored by the developer, so it ships even for a masked view. `nil`
|
|
38
|
+
// clears the declaration, which is what a removed or emptied prop sends.
|
|
39
|
+
RCT_CUSTOM_VIEW_PROPERTY(wireframeText, NSString *, RCTView)
|
|
40
|
+
{
|
|
41
|
+
NSString *text = [json isKindOfClass:[NSString class]] ? (NSString *)json : nil;
|
|
42
|
+
[MixpanelSwiftSessionReplay setMPWireframeTextWithValue:text view:view];
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
@end
|
|
@@ -18,8 +18,13 @@ import UIKit
|
|
|
18
18
|
MPSessionReplay.getInstance()?.captureScreenshot()
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/// - Parameter wireframeHandler: Destination for each captured wireframe's debug JSON.
|
|
22
|
+
/// Attached only when the payload's `debugOptions.emitWireframes` is set — this bridge's own
|
|
23
|
+
/// debug switch, read here rather than modelled by the SDK (see `BridgeDebugFlags`). Without
|
|
24
|
+
/// it the SDK builds no snapshot at all, so an app that has not asked pays nothing.
|
|
21
25
|
@objc public static func initialize(
|
|
22
26
|
_ token: String, distinctId: String, configJSON: String,
|
|
27
|
+
wireframeHandler: ((String) -> Void)? = nil,
|
|
23
28
|
completion: @escaping (Bool, NSError?) -> Void
|
|
24
29
|
) {
|
|
25
30
|
guard let data = configJSON.data(using: .utf8) else {
|
|
@@ -31,7 +36,20 @@ import UIKit
|
|
|
31
36
|
do {
|
|
32
37
|
APIConstants.setLibVersion(libVersion)
|
|
33
38
|
APIConstants.setMpLib(mpLib)
|
|
34
|
-
|
|
39
|
+
var config = try MPSessionReplayConfig.from(json: data)
|
|
40
|
+
|
|
41
|
+
// `emitWireframes` is this bridge's switch, not SDK configuration; the SDK ignores the
|
|
42
|
+
// key and the bridge attaches the destination the config could not carry.
|
|
43
|
+
let emitWireframes =
|
|
44
|
+
(try? JSONDecoder().decode(BridgeDebugFlags.self, from: data))?
|
|
45
|
+
.debugOptions?.emitWireframes ?? false
|
|
46
|
+
|
|
47
|
+
if let wireframeHandler, emitWireframes {
|
|
48
|
+
config.debugOptions?.wireframeEmitter = { snapshot in
|
|
49
|
+
wireframeHandler(snapshot.toJson())
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
MPSessionReplay.initialize(token: token, distinctId: distinctId, config: config) { result in
|
|
36
54
|
switch result {
|
|
37
55
|
case .success(_?):
|
|
@@ -86,6 +104,16 @@ import UIKit
|
|
|
86
104
|
view.mpReplaySensitive = value
|
|
87
105
|
}
|
|
88
106
|
|
|
107
|
+
/// Declares the text recorded for `view` in the wireframe.
|
|
108
|
+
///
|
|
109
|
+
/// Orthogonal to `setMPReplaySensitive` — the declared text is sent even when the view
|
|
110
|
+
/// is masked, because it is authored by the developer rather than scraped from the
|
|
111
|
+
/// screen. Pass `nil` (or a blank string) to clear it.
|
|
112
|
+
@objc public static func setMPWireframeText(value: String?, view: UIView) {
|
|
113
|
+
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
114
|
+
view.mpWireframeText = (trimmed?.isEmpty == false) ? trimmed : nil
|
|
115
|
+
}
|
|
116
|
+
|
|
89
117
|
private static func createError(_ message: String, code: Int = -1) -> NSError {
|
|
90
118
|
return NSError(
|
|
91
119
|
domain: "MixpanelSessionReplay",
|
|
@@ -112,3 +140,17 @@ import UIKit
|
|
|
112
140
|
}
|
|
113
141
|
}
|
|
114
142
|
}
|
|
143
|
+
|
|
144
|
+
/// The one key JS sends inside `debugOptions` that the SDK does not model.
|
|
145
|
+
///
|
|
146
|
+
/// `emitWireframes` is a bridge-level debug switch rather than SDK configuration: the SDK's
|
|
147
|
+
/// `DebugOptions.wireframeEmitter` is a closure and cannot cross the bridge as JSON, so JS sends
|
|
148
|
+
/// a boolean and the bridge supplies the callback. Keeping the flag here rather than in the SDK's
|
|
149
|
+
/// public `DebugOptions` leaves no inert knob on the native API surface.
|
|
150
|
+
private struct BridgeDebugFlags: Decodable {
|
|
151
|
+
struct DebugSection: Decodable {
|
|
152
|
+
var emitWireframes: Bool?
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
var debugOptions: DebugSection?
|
|
156
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["requireNativeComponent","MPSessionReplayView"],"sourceRoot":"../../src","sources":["MixpanelSessionReplayView.tsx"],"mappings":";;AAAA,SAASA,sBAAsB,QAAwB,cAAc;
|
|
1
|
+
{"version":3,"names":["requireNativeComponent","MPSessionReplayView"],"sourceRoot":"../../src","sources":["MixpanelSessionReplayView.tsx"],"mappings":";;AAAA,SAASA,sBAAsB,QAAwB,cAAc;AAuCrE,OAAO,MAAMC,mBAAmB,GAC9BD,sBAAsB,CACpB,2BACF,CAAC","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NativeModules","TurboModuleRegistry","isTurboModuleEnabled","global","__turboModuleProxy","MixpanelReactNativeSessionReplay","get","e","console","warn","errorMessage","Error"],"sourceRoot":"../../src","sources":["NativeMixpanelReactNativeSessionReplay.ts"],"mappings":";;AACA,SAASA,aAAa,EAAEC,mBAAmB,QAAQ,cAAc;;
|
|
1
|
+
{"version":3,"names":["NativeModules","TurboModuleRegistry","isTurboModuleEnabled","global","__turboModuleProxy","MixpanelReactNativeSessionReplay","get","e","console","warn","errorMessage","Error"],"sourceRoot":"../../src","sources":["NativeMixpanelReactNativeSessionReplay.ts"],"mappings":";;AACA,SAASA,aAAa,EAAEC,mBAAmB,QAAQ,cAAc;;AAwBjE;;AAKA;AACA;AACA,MAAMC,oBAAoB,GAAGC,MAAM,CAACC,kBAAkB,IAAI,IAAI;AAE9D,IAAIC,gCAA6C,GAAG,IAAI;AAExD,IAAIH,oBAAoB,EAAE;EACxB;EACA,IAAI;IACFG,gCAAgC,GAAGJ,mBAAmB,CAACK,GAAG,CACxD,kCACF,CAAC;EACH,CAAC,CAAC,OAAOC,CAAC,EAAE;IACV;IACAC,OAAO,CAACC,IAAI,CACV,4FACF,CAAC;EACH;AACF;;AAEA;AACA,IAAI,CAACJ,gCAAgC,EAAE;EACrCA,gCAAgC,GAC9BL,aAAa,CAACK,gCAAgC;AAClD;;AAEA;AACA,IAAI,CAACA,gCAAgC,EAAE;EACrC,MAAMK,YAAY,GAChB,+FAA+F,GAC/F,mJAAmJ,GACnJ,4DAA4D,GAC5D,2GAA2G,GAC3G,sFAAsF,GACtF,yEAAyE;EAE3E,MAAM,IAAIC,KAAK,CAACD,YAAY,CAAC;AAC/B;AAEA,eAAeL,gCAAgC","ignoreList":[]}
|