@mixpanel/react-native-session-replay 0.1.2
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/LICENSE +20 -0
- package/MixpanelReactNativeSessionReplay.podspec +26 -0
- package/README.md +200 -0
- package/android/app/build/generated/source/codegen/java/com/facebook/fbreact/specs/NativeMixpanelReactNativeSessionReplaySpec.java +54 -0
- package/android/app/build/generated/source/codegen/jni/CMakeLists.txt +36 -0
- package/android/app/build/generated/source/codegen/jni/MixpanelReactNativeSessionReplaySpec-generated.cpp +56 -0
- package/android/app/build/generated/source/codegen/jni/MixpanelReactNativeSessionReplaySpec.h +31 -0
- package/android/app/build/generated/source/codegen/jni/react/renderer/components/MixpanelReactNativeSessionReplaySpec/MixpanelReactNativeSessionReplaySpecJSI-generated.cpp +54 -0
- package/android/app/build/generated/source/codegen/jni/react/renderer/components/MixpanelReactNativeSessionReplaySpec/MixpanelReactNativeSessionReplaySpecJSI.h +107 -0
- package/android/build.gradle +89 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/mixpanelreactnativesessionreplay/MixpanelReactNativeSessionReplayModule.kt +287 -0
- package/android/src/main/java/com/mixpanelreactnativesessionreplay/MixpanelReactNativeSessionReplayPackage.kt +38 -0
- package/android/src/main/java/com/mixpanelreactnativesessionreplay/MixpanelSessionReplayView.kt +26 -0
- package/android/src/main/java/com/mixpanelreactnativesessionreplay/MixpanelSessionReplayViewManager.kt +29 -0
- package/ios/MixpanelReactNativeSessionReplay.h +5 -0
- package/ios/MixpanelReactNativeSessionReplay.mm +87 -0
- package/ios/MixpanelSessionReplayViewManager.mm +36 -0
- package/ios/MixpanelSwiftSessionReplay.swift +93 -0
- package/lib/module/MixpanelSessionReplayView.js +5 -0
- package/lib/module/MixpanelSessionReplayView.js.map +1 -0
- package/lib/module/NativeMixpanelReactNativeSessionReplay.js +5 -0
- package/lib/module/NativeMixpanelReactNativeSessionReplay.js.map +1 -0
- package/lib/module/index.js +188 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/MixpanelSessionReplayView.d.ts +7 -0
- package/lib/typescript/src/MixpanelSessionReplayView.d.ts.map +1 -0
- package/lib/typescript/src/NativeMixpanelReactNativeSessionReplay.d.ts +11 -0
- package/lib/typescript/src/NativeMixpanelReactNativeSessionReplay.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +117 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +171 -0
- package/src/MixpanelSessionReplayView.tsx +10 -0
- package/src/NativeMixpanelReactNativeSessionReplay.ts +19 -0
- package/src/index.tsx +220 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
package com.mixpanelreactnativesessionreplay
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
6
|
+
import com.mixpanel.android.sessionreplay.MPSessionReplay
|
|
7
|
+
import com.mixpanel.android.sessionreplay.MPSessionReplayInstance
|
|
8
|
+
import com.mixpanel.android.sessionreplay.MPSessionReplayError
|
|
9
|
+
import com.mixpanel.android.sessionreplay.models.MPSessionReplayConfig
|
|
10
|
+
import com.mixpanel.android.sessionreplay.sensitive_views.AutoMaskedView
|
|
11
|
+
import com.mixpanel.android.sessionreplay.utils.APIConstants
|
|
12
|
+
import org.json.JSONObject
|
|
13
|
+
import org.json.JSONArray
|
|
14
|
+
|
|
15
|
+
@ReactModule(name = MixpanelReactNativeSessionReplayModule.NAME)
|
|
16
|
+
class MixpanelReactNativeSessionReplayModule(reactContext: ReactApplicationContext) :
|
|
17
|
+
NativeMixpanelReactNativeSessionReplaySpec(reactContext) {
|
|
18
|
+
|
|
19
|
+
override fun getName(): String {
|
|
20
|
+
return NAME
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun initialize(token: String, distinctId: String, configJSON: String, promise: Promise) {
|
|
24
|
+
try {
|
|
25
|
+
if (token.isEmpty() || distinctId.isEmpty()) {
|
|
26
|
+
promise.reject("INVALID_CONFIG", "Token and distinctId are required")
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse JSON configuration
|
|
31
|
+
val config = if (configJSON.isNotEmpty()) {
|
|
32
|
+
try {
|
|
33
|
+
JSONObject(configJSON)
|
|
34
|
+
} catch (e: Exception) {
|
|
35
|
+
println("Mixpanel - Failed to parse config JSON: ${e.message}")
|
|
36
|
+
JSONObject()
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
JSONObject()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Configure Mixpanel Session Replay
|
|
43
|
+
|
|
44
|
+
val replayConfig = try {
|
|
45
|
+
MPSessionReplayConfig.fromJson(configJSON)
|
|
46
|
+
} catch (e: Exception) {
|
|
47
|
+
println("Mixpanel - Failed to parse config JSON in fromJson: ${e.message}")
|
|
48
|
+
promise.reject("CONFIG_JSON_PARSE_ERROR", "Failed to parse config JSON: ${e.message}", e)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
println("Mixpanel - Config variable fromJson: $replayConfig")
|
|
52
|
+
|
|
53
|
+
// Set library version and name
|
|
54
|
+
APIConstants.setLibVersion(LIB_VERSION)
|
|
55
|
+
APIConstants.setMpLib(MP_LIB)
|
|
56
|
+
|
|
57
|
+
// Initialize Mixpanel Session Replay
|
|
58
|
+
MPSessionReplay.initialize(
|
|
59
|
+
reactApplicationContext,
|
|
60
|
+
token,
|
|
61
|
+
distinctId,
|
|
62
|
+
replayConfig
|
|
63
|
+
) { result ->
|
|
64
|
+
result.fold(
|
|
65
|
+
onSuccess = { sessionReplayInstance ->
|
|
66
|
+
// Successfully initialized
|
|
67
|
+
sessionReplayInstance?.let { instance ->
|
|
68
|
+
try {
|
|
69
|
+
// Configure AutoMaskedView categories with comprehensive React Native support
|
|
70
|
+
configureSensitiveClasses(instance, replayConfig)
|
|
71
|
+
println("Mixpanel - Session Replay initialized successfully with enhanced view masking")
|
|
72
|
+
promise.resolve(null)
|
|
73
|
+
} catch (e: Exception) {
|
|
74
|
+
println("Mixpanel - Error configuring sensitive classes: ${e.message}")
|
|
75
|
+
// Still resolve as initialization succeeded, but log the configuration error
|
|
76
|
+
promise.resolve(null)
|
|
77
|
+
}
|
|
78
|
+
} ?: run {
|
|
79
|
+
promise.reject(
|
|
80
|
+
"INITIALIZATION_ERROR",
|
|
81
|
+
"Session Replay instance is null after successful initialization"
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
onFailure = { error ->
|
|
86
|
+
when (error) {
|
|
87
|
+
is MPSessionReplayError.Disabled -> {
|
|
88
|
+
promise.reject("SESSION_REPLAY_DISABLED", error.reason)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
is MPSessionReplayError.InitializationError -> {
|
|
92
|
+
promise.reject("INITIALIZATION_ERROR", error.cause.message, error.cause)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
else -> {
|
|
96
|
+
promise.reject(
|
|
97
|
+
"UNKNOWN_ERROR",
|
|
98
|
+
"An unknown error occurred during initialization",
|
|
99
|
+
error
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
} catch (e: Exception) {
|
|
107
|
+
promise.reject("INITIALIZATION_ERROR", e.message, e)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override fun startRecording(recordingSessionsPercent: Double, promise: Promise) {
|
|
112
|
+
try {
|
|
113
|
+
MPSessionReplay.getInstance()?.startRecording(sessionsPercent = recordingSessionsPercent)
|
|
114
|
+
promise.resolve(null)
|
|
115
|
+
} catch (e: Exception) {
|
|
116
|
+
promise.reject("START_RECORDING_ERROR", e.message, e)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
override fun stopRecording(promise: Promise) {
|
|
121
|
+
try {
|
|
122
|
+
MPSessionReplay.getInstance()?.stopRecording()
|
|
123
|
+
promise.resolve(null)
|
|
124
|
+
} catch (e: Exception) {
|
|
125
|
+
promise.reject("STOP_RECORDING_ERROR", e.message, e)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override fun isRecording(promise: Promise) {
|
|
130
|
+
try {
|
|
131
|
+
val recording = MPSessionReplay.getInstance()?.isRecording() ?: false
|
|
132
|
+
promise.resolve(recording)
|
|
133
|
+
} catch (e: Exception) {
|
|
134
|
+
promise.reject("IS_RECORDING_ERROR", e.message, e)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override fun identify(distinctId: String, promise: Promise) {
|
|
139
|
+
try {
|
|
140
|
+
if (distinctId.isEmpty()) {
|
|
141
|
+
promise.reject("INVALID_DISTINCT_ID", "distinctId is required")
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
MPSessionReplay.getInstance()?.identify(distinctId)
|
|
146
|
+
promise.resolve(null)
|
|
147
|
+
} catch (e: Exception) {
|
|
148
|
+
promise.reject("IDENTIFY_ERROR", e.message, e)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Safely configure sensitive classes for comprehensive React Native view masking
|
|
155
|
+
* Supports both old and new React Native architectures with crash prevention
|
|
156
|
+
*/
|
|
157
|
+
private fun configureSensitiveClasses(
|
|
158
|
+
mpInstance: MPSessionReplayInstance,
|
|
159
|
+
config: MPSessionReplayConfig
|
|
160
|
+
) {
|
|
161
|
+
// Always mask input text classes to protect user input fields
|
|
162
|
+
configureInputTextClasses(mpInstance)
|
|
163
|
+
|
|
164
|
+
if (config.autoMaskedViews.contains(AutoMaskedView.Text)) {
|
|
165
|
+
configureTextClasses(mpInstance)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (config.autoMaskedViews.contains(AutoMaskedView.Image)) {
|
|
169
|
+
configureImageClasses(mpInstance)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (config.autoMaskedViews.contains(AutoMaskedView.Web)) {
|
|
173
|
+
configureWebClasses(mpInstance)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Configure text display view classes for masking
|
|
179
|
+
*/
|
|
180
|
+
private fun configureTextClasses(mpInstance: MPSessionReplayInstance) {
|
|
181
|
+
val textClasses = listOf(
|
|
182
|
+
// React Native specific text display views
|
|
183
|
+
"com.facebook.react.views.text.ReactTextView",
|
|
184
|
+
"com.facebook.react.views.text.ReactTextAnchorView",
|
|
185
|
+
|
|
186
|
+
// New Architecture (Fabric) text display views
|
|
187
|
+
"com.facebook.react.fabric.views.text.ReactTextView"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
textClasses.forEach { className ->
|
|
191
|
+
safeAddSensitiveClass(mpInstance, className, "Text")
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Configure text input view classes for masking
|
|
197
|
+
*/
|
|
198
|
+
private fun configureInputTextClasses(mpInstance: MPSessionReplayInstance) {
|
|
199
|
+
val inputTextClasses = listOf(
|
|
200
|
+
// React Native specific text input views
|
|
201
|
+
"com.facebook.react.views.textinput.ReactEditText",
|
|
202
|
+
|
|
203
|
+
// New Architecture (Fabric) text input views
|
|
204
|
+
"com.facebook.react.fabric.views.textinput.ReactEditText"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
inputTextClasses.forEach { className ->
|
|
208
|
+
safeAddSensitiveClass(mpInstance, className, "Input Text")
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Configure image-related view classes for masking
|
|
214
|
+
*/
|
|
215
|
+
private fun configureImageClasses(mpInstance: MPSessionReplayInstance) {
|
|
216
|
+
val imageClasses = listOf(
|
|
217
|
+
// React Native specific image views
|
|
218
|
+
"com.facebook.react.views.image.ReactImageView",
|
|
219
|
+
"com.facebook.react.views.text.frescosupport.FrescoBasedReactTextInlineImageSpan",
|
|
220
|
+
|
|
221
|
+
// New Architecture (Fabric) image views
|
|
222
|
+
"com.facebook.react.fabric.views.image.ReactImageView",
|
|
223
|
+
|
|
224
|
+
// Common third-party image libraries
|
|
225
|
+
"com.facebook.drawee.view.SimpleDraweeView"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
imageClasses.forEach { className ->
|
|
229
|
+
safeAddSensitiveClass(mpInstance, className, "Image")
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Configure web-related view classes for masking
|
|
235
|
+
*/
|
|
236
|
+
private fun configureWebClasses(mpInstance: MPSessionReplayInstance) {
|
|
237
|
+
val webClasses = listOf(
|
|
238
|
+
// React Native WebView (community package)
|
|
239
|
+
"com.reactnativecommunity.webview.RNCWebView",
|
|
240
|
+
"com.reactnativecommunity.webview.RNCWebViewManager.RNCWebView",
|
|
241
|
+
|
|
242
|
+
// Legacy React Native WebView
|
|
243
|
+
"com.facebook.react.views.webview.ReactWebView",
|
|
244
|
+
|
|
245
|
+
// New Architecture (Fabric) web views
|
|
246
|
+
"com.facebook.react.fabric.views.webview.ReactWebView"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
webClasses.forEach { className ->
|
|
250
|
+
safeAddSensitiveClass(mpInstance, className, "Web")
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Safely add a sensitive class with comprehensive error handling
|
|
256
|
+
* Prevents crashes from ClassNotFoundException or other reflection issues
|
|
257
|
+
*/
|
|
258
|
+
private fun safeAddSensitiveClass(
|
|
259
|
+
mpInstance: MPSessionReplayInstance,
|
|
260
|
+
className: String,
|
|
261
|
+
category: String
|
|
262
|
+
) {
|
|
263
|
+
try {
|
|
264
|
+
val clazz = Class.forName(className)
|
|
265
|
+
mpInstance.addSensitiveClass(clazz)
|
|
266
|
+
println("Mixpanel - Successfully added $category class: $className")
|
|
267
|
+
} catch (e: ClassNotFoundException) {
|
|
268
|
+
// Class not found - this is expected for architecture-specific or optional classes
|
|
269
|
+
println("Mixpanel - $category class not available (expected): $className")
|
|
270
|
+
} catch (e: NoClassDefFoundError) {
|
|
271
|
+
// Missing dependencies - log but don't crash
|
|
272
|
+
println("Mixpanel - $category class dependencies not available: $className - ${e.message}")
|
|
273
|
+
} catch (e: LinkageError) {
|
|
274
|
+
// Linking issues - log but don't crash
|
|
275
|
+
println("Mixpanel - $category class linking error: $className - ${e.message}")
|
|
276
|
+
} catch (e: Exception) {
|
|
277
|
+
// Any other reflection-related issues
|
|
278
|
+
println("Mixpanel - Unexpected error adding $category class $className: ${e.message}")
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
companion object {
|
|
283
|
+
const val NAME = "MixpanelReactNativeSessionReplay"
|
|
284
|
+
const val LIB_VERSION = "0.1.1"
|
|
285
|
+
const val MP_LIB = "react-native-sr"
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.mixpanelreactnativesessionreplay
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager
|
|
9
|
+
import java.util.HashMap
|
|
10
|
+
|
|
11
|
+
class MixpanelReactNativeSessionReplayPackage : BaseReactPackage() {
|
|
12
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
13
|
+
return if (name == MixpanelReactNativeSessionReplayModule.NAME) {
|
|
14
|
+
MixpanelReactNativeSessionReplayModule(reactContext)
|
|
15
|
+
} else {
|
|
16
|
+
null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
21
|
+
return listOf(MixpanelSessionReplayViewManager())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
25
|
+
return ReactModuleInfoProvider {
|
|
26
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
27
|
+
moduleInfos[MixpanelReactNativeSessionReplayModule.NAME] = ReactModuleInfo(
|
|
28
|
+
MixpanelReactNativeSessionReplayModule.NAME,
|
|
29
|
+
MixpanelReactNativeSessionReplayModule.NAME,
|
|
30
|
+
false, // canOverrideExistingModule
|
|
31
|
+
false, // needsEagerInit
|
|
32
|
+
false, // isCxxModule
|
|
33
|
+
true // isTurboModule
|
|
34
|
+
)
|
|
35
|
+
moduleInfos
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/android/src/main/java/com/mixpanelreactnativesessionreplay/MixpanelSessionReplayView.kt
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.mixpanelreactnativesessionreplay
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
5
|
+
import com.mixpanel.android.sessionreplay.MPSessionReplay
|
|
6
|
+
|
|
7
|
+
class MixpanelSessionReplayView(context: Context) : ReactViewGroup(context) {
|
|
8
|
+
private var isSensitive: Boolean = false
|
|
9
|
+
|
|
10
|
+
fun setSensitive(sensitive: Boolean) {
|
|
11
|
+
if (isSensitive == sensitive) return
|
|
12
|
+
|
|
13
|
+
isSensitive = sensitive
|
|
14
|
+
updateViewSensitivity()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private fun updateViewSensitivity() {
|
|
18
|
+
MPSessionReplay.getInstance()?.let { sessionReplay ->
|
|
19
|
+
if (isSensitive) {
|
|
20
|
+
sessionReplay.addSensitiveView(this)
|
|
21
|
+
} else {
|
|
22
|
+
sessionReplay.addSafeView(this)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package com.mixpanelreactnativesessionreplay
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
7
|
+
|
|
8
|
+
@ReactModule(name = MixpanelSessionReplayViewManager.REACT_CLASS)
|
|
9
|
+
class MixpanelSessionReplayViewManager : ViewGroupManager<MixpanelSessionReplayView>() {
|
|
10
|
+
|
|
11
|
+
companion object {
|
|
12
|
+
const val REACT_CLASS = "MixpanelSessionReplayView"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
override fun getName(): String {
|
|
16
|
+
return REACT_CLASS
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun createViewInstance(reactContext: ThemedReactContext): MixpanelSessionReplayView {
|
|
20
|
+
return MixpanelSessionReplayView(reactContext)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@ReactProp(name = "sensitive")
|
|
24
|
+
fun setSensitive(view: MixpanelSessionReplayView, sensitive: Boolean) {
|
|
25
|
+
// log the sensitive property change
|
|
26
|
+
println("MixpanelSessionReplayView - setSensitive called with value: $sensitive for view: $view")
|
|
27
|
+
view.setSensitive(sensitive)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#import "MixpanelReactNativeSessionReplay.h"
|
|
2
|
+
#if __has_include("MixpanelReactNativeSessionReplay-Swift.h")
|
|
3
|
+
#import <MixpanelReactNativeSessionReplay-Swift.h>
|
|
4
|
+
#else
|
|
5
|
+
#import <MixpanelReactNativeSessionReplay/MixpanelReactNativeSessionReplay-Swift.h>
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
@implementation MixpanelReactNativeSessionReplay
|
|
9
|
+
RCT_EXPORT_MODULE()
|
|
10
|
+
|
|
11
|
+
- (void)initialize:(nonnull NSString *)token distinctId:(nonnull NSString *)distinctId configJSON:(nonnull NSString *)configJSON resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
12
|
+
@try {
|
|
13
|
+
if (!token || !distinctId) {
|
|
14
|
+
reject(@"INVALID_CONFIG", @"Token and distinctId are required", nil);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
[MixpanelSwiftSessionReplay initialize:token distinctId:distinctId configJSON:configJSON completion:^(BOOL success, NSError * _Nullable error) {
|
|
18
|
+
if (success) {
|
|
19
|
+
resolve(nil);
|
|
20
|
+
} else {
|
|
21
|
+
reject(@"INITIALIZATION_FAILED", error.debugDescription, error);
|
|
22
|
+
}
|
|
23
|
+
}];
|
|
24
|
+
}
|
|
25
|
+
@catch (NSException *exception) {
|
|
26
|
+
reject(@"INITIALIZATION_ERROR", exception.reason, nil);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
- (void)startRecording:(double)recordingSessionsPercent
|
|
31
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
32
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
33
|
+
@try {
|
|
34
|
+
[MixpanelSwiftSessionReplay startRecordingWithRecordingSessionsPercent:recordingSessionsPercent];
|
|
35
|
+
resolve(nil);
|
|
36
|
+
}
|
|
37
|
+
@catch (NSException *exception) {
|
|
38
|
+
reject(@"START_RECORDING_ERROR", exception.reason, nil);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
- (void)stopRecording:(RCTPromiseResolveBlock)resolve
|
|
43
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
44
|
+
@try {
|
|
45
|
+
[MixpanelSwiftSessionReplay stopRecording];
|
|
46
|
+
resolve(nil);
|
|
47
|
+
}
|
|
48
|
+
@catch (NSException *exception) {
|
|
49
|
+
reject(@"STOP_RECORDING_ERROR", exception.reason, nil);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (void)isRecording:(RCTPromiseResolveBlock)resolve
|
|
54
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
55
|
+
@try {
|
|
56
|
+
BOOL recording = [MixpanelSwiftSessionReplay isRecording];
|
|
57
|
+
resolve(@(recording));
|
|
58
|
+
}
|
|
59
|
+
@catch (NSException *exception) {
|
|
60
|
+
reject(@"IS_RECORDING_ERROR", exception.reason, nil);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)identify:(NSString *)distinctId
|
|
65
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
66
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
67
|
+
@try {
|
|
68
|
+
if (!distinctId || [distinctId isEqualToString: @""]) {
|
|
69
|
+
reject(@"INVALID_DISTINCT_ID", @"distinctId is required", nil);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[MixpanelSwiftSessionReplay identify:distinctId];
|
|
74
|
+
resolve(nil);
|
|
75
|
+
}
|
|
76
|
+
@catch (NSException *exception) {
|
|
77
|
+
reject(@"IDENTIFY_ERROR", exception.reason, nil);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
82
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
83
|
+
{
|
|
84
|
+
return std::make_shared<facebook::react::NativeMixpanelReactNativeSessionReplaySpecJSI>(params);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
#import <React/RCTViewManager.h>
|
|
3
|
+
#import <React/RCTView.h>
|
|
4
|
+
|
|
5
|
+
#if __has_include("MixpanelReactNativeSessionReplay-Swift.h")
|
|
6
|
+
#import <MixpanelReactNativeSessionReplay-Swift.h>
|
|
7
|
+
#else
|
|
8
|
+
#import <MixpanelReactNativeSessionReplay/MixpanelReactNativeSessionReplay-Swift.h>
|
|
9
|
+
#endif
|
|
10
|
+
|
|
11
|
+
@interface MixpanelSessionReplayViewManager : RCTViewManager
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
@implementation MixpanelSessionReplayViewManager
|
|
15
|
+
|
|
16
|
+
RCT_EXPORT_MODULE(MixpanelSessionReplayView)
|
|
17
|
+
|
|
18
|
+
- (UIView *)view
|
|
19
|
+
{
|
|
20
|
+
return [[RCTView alloc] init];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
RCT_CUSTOM_VIEW_PROPERTY(sensitive, BOOL, RCTView)
|
|
24
|
+
{
|
|
25
|
+
if (json == nil) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ([json boolValue] == YES) {
|
|
30
|
+
[MixpanelSwiftSessionReplay setMPReplaySensitiveWithValue:YES view:view];
|
|
31
|
+
} else if ([json boolValue] == NO) {
|
|
32
|
+
[MixpanelSwiftSessionReplay setMPReplaySensitiveWithValue:NO view:view];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import MixpanelSessionReplay
|
|
4
|
+
|
|
5
|
+
@objc public class MixpanelSwiftSessionReplay: NSObject {
|
|
6
|
+
static let libVersion = "0.1.1"
|
|
7
|
+
static let mpLib = "react-native-sr"
|
|
8
|
+
|
|
9
|
+
@objc public static func startRecording(recordingSessionsPercent: Double = 100.0) {
|
|
10
|
+
MPSessionReplay.getInstance()?.startRecording(sessionsPercent: recordingSessionsPercent)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@objc public static func stopRecording() {
|
|
14
|
+
MPSessionReplay.getInstance()?.stopRecording()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@objc public static func captureScreenshot() {
|
|
18
|
+
MPSessionReplay.getInstance()?.captureScreenshot()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@objc public static func initialize(_ token: String, distinctId: String, configJSON: String, completion: @escaping (Bool, NSError?) -> Void) {
|
|
22
|
+
guard let data = configJSON.data(using: .utf8) else {
|
|
23
|
+
let error = createError("Invalid config JSON string", code: 3840)
|
|
24
|
+
completion(false, error)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
do {
|
|
29
|
+
APIConstants.setLibVersion(libVersion)
|
|
30
|
+
APIConstants.setMpLib(mpLib)
|
|
31
|
+
let config = try MPSessionReplayConfig.from(json: data)
|
|
32
|
+
MPSessionReplay.initialize(token: token, distinctId: distinctId, config: config) { result in
|
|
33
|
+
switch result {
|
|
34
|
+
case .success(_?):
|
|
35
|
+
setSensitiveClasses(config: config)
|
|
36
|
+
completion(true, nil)
|
|
37
|
+
|
|
38
|
+
case .success(nil):
|
|
39
|
+
completion(false, createError("Instance found nil after successful initialisation."))
|
|
40
|
+
|
|
41
|
+
case .failure(let error as MPSessionReplayError):
|
|
42
|
+
let message = switch error {
|
|
43
|
+
case .disabledByRemoteSetting(let message): message
|
|
44
|
+
case .failedToInitialize: "Failed to initialize the SDK: \(error)"
|
|
45
|
+
}
|
|
46
|
+
completion(false, createError(message))
|
|
47
|
+
|
|
48
|
+
case .failure(let error):
|
|
49
|
+
completion(false, createError("Session replay initialization failed: \(error)"))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
completion(false, error as NSError)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@objc public static func isRecording() -> Bool {
|
|
58
|
+
return MPSessionReplay.getInstance()?.isRecording ?? false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@objc public static func identify(_ distinctId: String) {
|
|
62
|
+
MPSessionReplay.getInstance()?.identify(distinctId: distinctId)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@objc public static func setMPReplaySensitive(value: Bool, view: UIView) {
|
|
66
|
+
view.mpReplaySensitive = value
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private static func createError(_ message: String, code: Int = -1) -> NSError {
|
|
70
|
+
return NSError(domain: "MixpanelSessionReplay",
|
|
71
|
+
code: code,
|
|
72
|
+
userInfo: [NSLocalizedDescriptionKey: message])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private static func setSensitiveClasses(config: MPSessionReplayConfig) {
|
|
76
|
+
let legacyTextViewClass: AnyClass? = NSClassFromString("RCTTextView")
|
|
77
|
+
let fabricTextViewClass: AnyClass? = NSClassFromString("RCTParagraphTextView")
|
|
78
|
+
let imageViewClass: AnyClass? = NSClassFromString("RCTImageView")
|
|
79
|
+
let sessionReplay = MPSessionReplay.getInstance()
|
|
80
|
+
|
|
81
|
+
if let imageViewClass, config.autoMaskedViews.contains(.image) {
|
|
82
|
+
sessionReplay?.addSensitiveClass(imageViewClass)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if let fabricTextViewClass, config.autoMaskedViews.contains(.text) {
|
|
86
|
+
sessionReplay?.addSensitiveClass(fabricTextViewClass)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if let legacyTextViewClass, config.autoMaskedViews.contains(.text) {
|
|
90
|
+
sessionReplay?.addSensitiveClass(legacyTextViewClass)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["requireNativeComponent","MixpanelSessionReplayView"],"sourceRoot":"../../src","sources":["MixpanelSessionReplayView.tsx"],"mappings":";;AAAA,SAASA,sBAAsB,QAAwB,cAAc;AAMrE,OAAO,MAAMC,yBAAyB,GACpCD,sBAAsB,CACpB,2BACF,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeMixpanelReactNativeSessionReplay.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAelD,eAAeA,mBAAmB,CAACC,YAAY,CAC7C,kCACF,CAAC","ignoreList":[]}
|