@posthog/react-native-plugin 0.0.1 → 2.0.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/LICENSE +353 -0
- package/README.md +21 -35
- package/android/build.gradle +90 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/posthogreactnativeplugin/PosthogReactNativePluginModule.kt +317 -0
- package/android/src/main/java/com/posthogreactnativeplugin/PosthogReactNativePluginPackage.kt +13 -0
- package/ios/PosthogReactNativePlugin-Bridging-Header.h +2 -0
- package/ios/PosthogReactNativePlugin.mm +45 -0
- package/ios/PosthogReactNativePlugin.swift +254 -0
- package/lib/commonjs/index.js +60 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/index.js +48 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +39 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/index.d.ts +39 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/package.json +112 -7
- package/posthog-react-native-plugin.podspec +68 -0
- package/src/index.tsx +119 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
package com.posthogreactnativeplugin
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
7
|
+
import com.facebook.react.bridge.ReactMethod
|
|
8
|
+
import com.facebook.react.bridge.ReadableMap
|
|
9
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
10
|
+
import com.posthog.PostHog
|
|
11
|
+
import com.posthog.PostHogConfig
|
|
12
|
+
import com.posthog.PostHogEvent
|
|
13
|
+
import com.posthog.android.PostHogAndroid
|
|
14
|
+
import com.posthog.android.PostHogAndroidConfig
|
|
15
|
+
import com.posthog.internal.PostHogPreferences
|
|
16
|
+
import com.posthog.internal.PostHogPreferences.Companion.ANONYMOUS_ID
|
|
17
|
+
import com.posthog.internal.PostHogPreferences.Companion.DISTINCT_ID
|
|
18
|
+
import com.posthog.internal.PostHogSessionManager
|
|
19
|
+
import java.util.UUID
|
|
20
|
+
|
|
21
|
+
class PosthogReactNativePluginModule(
|
|
22
|
+
reactContext: ReactApplicationContext,
|
|
23
|
+
) : ReactContextBaseJavaModule(reactContext) {
|
|
24
|
+
override fun getName(): String = NAME
|
|
25
|
+
|
|
26
|
+
@ReactMethod
|
|
27
|
+
fun setup(
|
|
28
|
+
sessionId: String,
|
|
29
|
+
sdkOptions: ReadableMap,
|
|
30
|
+
pluginConfig: ReadableMap,
|
|
31
|
+
promise: Promise,
|
|
32
|
+
) {
|
|
33
|
+
val sessionReplayConfig = getMap(pluginConfig, "sessionReplay")
|
|
34
|
+
val errorTrackingConfig = getMap(pluginConfig, "errorTracking")
|
|
35
|
+
|
|
36
|
+
setupNativeSdk(
|
|
37
|
+
method = "setup",
|
|
38
|
+
sessionId = sessionId,
|
|
39
|
+
sdkOptions = sdkOptions,
|
|
40
|
+
sessionReplayEnabled = getBoolean(sessionReplayConfig, "enabled", false),
|
|
41
|
+
sdkReplayConfig = getMap(sessionReplayConfig, "sdkReplayConfig"),
|
|
42
|
+
decideReplayConfig = getMap(sessionReplayConfig, "decideReplayConfig"),
|
|
43
|
+
nativeErrorTrackingAutocapture = getBoolean(errorTrackingConfig, "nativeAutocapture", false),
|
|
44
|
+
promise = promise,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@ReactMethod
|
|
49
|
+
fun start(
|
|
50
|
+
sessionId: String,
|
|
51
|
+
sdkOptions: ReadableMap,
|
|
52
|
+
sdkReplayConfig: ReadableMap,
|
|
53
|
+
decideReplayConfig: ReadableMap,
|
|
54
|
+
promise: Promise,
|
|
55
|
+
) {
|
|
56
|
+
setupNativeSdk(
|
|
57
|
+
method = "start",
|
|
58
|
+
sessionId = sessionId,
|
|
59
|
+
sdkOptions = sdkOptions,
|
|
60
|
+
sessionReplayEnabled = true,
|
|
61
|
+
sdkReplayConfig = sdkReplayConfig,
|
|
62
|
+
decideReplayConfig = decideReplayConfig,
|
|
63
|
+
nativeErrorTrackingAutocapture = false,
|
|
64
|
+
promise = promise,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun setupNativeSdk(
|
|
69
|
+
method: String,
|
|
70
|
+
sessionId: String,
|
|
71
|
+
sdkOptions: ReadableMap,
|
|
72
|
+
sessionReplayEnabled: Boolean,
|
|
73
|
+
sdkReplayConfig: ReadableMap?,
|
|
74
|
+
decideReplayConfig: ReadableMap?,
|
|
75
|
+
nativeErrorTrackingAutocapture: Boolean,
|
|
76
|
+
promise: Promise,
|
|
77
|
+
) {
|
|
78
|
+
val initRunnable =
|
|
79
|
+
Runnable {
|
|
80
|
+
try {
|
|
81
|
+
val uuid = UUID.fromString(sessionId)
|
|
82
|
+
PostHogSessionManager.setSessionId(uuid)
|
|
83
|
+
|
|
84
|
+
val context = this.reactApplicationContext
|
|
85
|
+
val apiKey = getString(sdkOptions, "apiKey", "")
|
|
86
|
+
val host = getString(sdkOptions, "host", PostHogConfig.DEFAULT_HOST)
|
|
87
|
+
val debugValue = getBoolean(sdkOptions, "debug", false)
|
|
88
|
+
val distinctId = getString(sdkOptions, "distinctId", "")
|
|
89
|
+
val anonymousId = getString(sdkOptions, "anonymousId", "")
|
|
90
|
+
val theSdkVersion = getString(sdkOptions, "sdkVersion", "")
|
|
91
|
+
val theFlushAt = getInt(sdkOptions, "flushAt", DEFAULT_FLUSH_AT)
|
|
92
|
+
|
|
93
|
+
val config =
|
|
94
|
+
PostHogAndroidConfig(apiKey, host).apply {
|
|
95
|
+
debug = debugValue
|
|
96
|
+
captureDeepLinks = false
|
|
97
|
+
captureApplicationLifecycleEvents = false
|
|
98
|
+
captureScreenViews = false
|
|
99
|
+
flushAt = theFlushAt
|
|
100
|
+
errorTrackingConfig.autoCapture = nativeErrorTrackingAutocapture
|
|
101
|
+
|
|
102
|
+
// React Native rethrows fatal JS errors natively as JavascriptException.
|
|
103
|
+
// The JS layer already captured them, so drop the native duplicate.
|
|
104
|
+
addBeforeSend { event -> if (isReactNativeFatalJsError(event)) null else event }
|
|
105
|
+
|
|
106
|
+
// Always apply the session replay configuration so that recording started later
|
|
107
|
+
// (e.g. startRecording or a linked feature flag) uses the right mode and masking;
|
|
108
|
+
// sessionReplayEnabled only controls whether recording starts at setup.
|
|
109
|
+
val maskAllTextInputs = getBoolean(sdkReplayConfig, "maskAllTextInputs", DEFAULT_MASK_ALL_TEXT_INPUTS)
|
|
110
|
+
val maskAllImages = getBoolean(sdkReplayConfig, "maskAllImages", DEFAULT_MASK_ALL_IMAGES)
|
|
111
|
+
val captureLog = getBoolean(sdkReplayConfig, "captureLog", DEFAULT_CAPTURE_LOG)
|
|
112
|
+
|
|
113
|
+
// read throttleDelayMs and use androidDebouncerDelayMs as a fallback for back compatibility
|
|
114
|
+
val throttleDelayMs =
|
|
115
|
+
when {
|
|
116
|
+
hasKey(sdkReplayConfig, "throttleDelayMs") -> getInt(sdkReplayConfig, "throttleDelayMs", DEFAULT_THROTTLE_DELAY_MS)
|
|
117
|
+
hasKey(sdkReplayConfig, "androidDebouncerDelayMs") -> getInt(sdkReplayConfig, "androidDebouncerDelayMs", DEFAULT_THROTTLE_DELAY_MS)
|
|
118
|
+
else -> DEFAULT_THROTTLE_DELAY_MS
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
sessionReplay = sessionReplayEnabled
|
|
122
|
+
sessionReplayConfig.screenshot = true
|
|
123
|
+
sessionReplayConfig.captureLogcat = captureLog
|
|
124
|
+
sessionReplayConfig.throttleDelayMs = throttleDelayMs.toLong()
|
|
125
|
+
sessionReplayConfig.maskAllImages = maskAllImages
|
|
126
|
+
sessionReplayConfig.maskAllTextInputs = maskAllTextInputs
|
|
127
|
+
sessionReplayConfig.sampleRate = getDoubleOrNull(sdkReplayConfig, "sampleRate")
|
|
128
|
+
|
|
129
|
+
val endpoint = getString(decideReplayConfig, "endpoint", "")
|
|
130
|
+
if (endpoint.isNotEmpty()) {
|
|
131
|
+
snapshotEndpoint = endpoint
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (theSdkVersion.isNotEmpty()) {
|
|
135
|
+
sdkName = "posthog-react-native"
|
|
136
|
+
sdkVersion = theSdkVersion
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
PostHogAndroid.setup(context, config)
|
|
140
|
+
|
|
141
|
+
setIdentify(config.cachePreferences, distinctId, anonymousId)
|
|
142
|
+
} catch (e: Throwable) {
|
|
143
|
+
logError(method, e)
|
|
144
|
+
} finally {
|
|
145
|
+
promise.resolve(null)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// forces the SDK to be initialized on the main thread
|
|
150
|
+
if (UiThreadUtil.isOnUiThread()) {
|
|
151
|
+
initRunnable.run()
|
|
152
|
+
} else {
|
|
153
|
+
UiThreadUtil.runOnUiThread(initRunnable)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@ReactMethod
|
|
158
|
+
fun startSession(
|
|
159
|
+
sessionId: String,
|
|
160
|
+
promise: Promise,
|
|
161
|
+
) {
|
|
162
|
+
try {
|
|
163
|
+
val uuid = UUID.fromString(sessionId)
|
|
164
|
+
PostHogSessionManager.setSessionId(uuid)
|
|
165
|
+
PostHog.startSession()
|
|
166
|
+
} catch (e: Throwable) {
|
|
167
|
+
logError("startSession", e)
|
|
168
|
+
} finally {
|
|
169
|
+
promise.resolve(null)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@ReactMethod
|
|
174
|
+
fun isEnabled(promise: Promise) {
|
|
175
|
+
try {
|
|
176
|
+
promise.resolve(PostHog.isSessionReplayActive())
|
|
177
|
+
} catch (e: Throwable) {
|
|
178
|
+
logError("isEnabled", e)
|
|
179
|
+
promise.resolve(false)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@ReactMethod
|
|
184
|
+
fun endSession(promise: Promise) {
|
|
185
|
+
try {
|
|
186
|
+
PostHog.endSession()
|
|
187
|
+
} catch (e: Throwable) {
|
|
188
|
+
logError("endSession", e)
|
|
189
|
+
} finally {
|
|
190
|
+
promise.resolve(null)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@ReactMethod
|
|
195
|
+
fun identify(
|
|
196
|
+
distinctId: String,
|
|
197
|
+
anonymousId: String,
|
|
198
|
+
promise: Promise,
|
|
199
|
+
) {
|
|
200
|
+
try {
|
|
201
|
+
setIdentify(PostHog.getConfig<PostHogConfig>()?.cachePreferences, distinctId, anonymousId)
|
|
202
|
+
} catch (e: Throwable) {
|
|
203
|
+
logError("identify", e)
|
|
204
|
+
} finally {
|
|
205
|
+
promise.resolve(null)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private fun setIdentify(
|
|
210
|
+
cachePreferences: PostHogPreferences?,
|
|
211
|
+
distinctId: String,
|
|
212
|
+
anonymousId: String,
|
|
213
|
+
) {
|
|
214
|
+
cachePreferences?.let { preferences ->
|
|
215
|
+
if (anonymousId.isNotEmpty()) {
|
|
216
|
+
preferences.setValue(ANONYMOUS_ID, anonymousId)
|
|
217
|
+
}
|
|
218
|
+
if (distinctId.isNotEmpty()) {
|
|
219
|
+
preferences.setValue(DISTINCT_ID, distinctId)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@ReactMethod
|
|
225
|
+
fun startRecording(
|
|
226
|
+
resumeCurrent: Boolean,
|
|
227
|
+
promise: Promise,
|
|
228
|
+
) {
|
|
229
|
+
try {
|
|
230
|
+
PostHog.startSessionReplay(resumeCurrent)
|
|
231
|
+
} catch (e: Throwable) {
|
|
232
|
+
logError("startRecording", e)
|
|
233
|
+
} finally {
|
|
234
|
+
promise.resolve(null)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@ReactMethod
|
|
239
|
+
fun stopRecording(promise: Promise) {
|
|
240
|
+
try {
|
|
241
|
+
PostHog.stopSessionReplay()
|
|
242
|
+
} catch (e: Throwable) {
|
|
243
|
+
logError("stopRecording", e)
|
|
244
|
+
} finally {
|
|
245
|
+
promise.resolve(null)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private fun getMap(
|
|
250
|
+
map: ReadableMap?,
|
|
251
|
+
key: String,
|
|
252
|
+
): ReadableMap? =
|
|
253
|
+
runCatching {
|
|
254
|
+
if (map != null && map.hasKey(key) && !map.isNull(key)) {
|
|
255
|
+
map.getMap(key)
|
|
256
|
+
} else {
|
|
257
|
+
null
|
|
258
|
+
}
|
|
259
|
+
}.getOrNull()
|
|
260
|
+
|
|
261
|
+
private fun hasKey(
|
|
262
|
+
map: ReadableMap?,
|
|
263
|
+
key: String,
|
|
264
|
+
): Boolean = runCatching { map != null && map.hasKey(key) && !map.isNull(key) }.getOrDefault(false)
|
|
265
|
+
|
|
266
|
+
private fun getBoolean(
|
|
267
|
+
map: ReadableMap?,
|
|
268
|
+
key: String,
|
|
269
|
+
default: Boolean,
|
|
270
|
+
): Boolean = runCatching { if (hasKey(map, key)) map?.getBoolean(key) ?: default else default }.getOrDefault(default)
|
|
271
|
+
|
|
272
|
+
private fun getString(
|
|
273
|
+
map: ReadableMap?,
|
|
274
|
+
key: String,
|
|
275
|
+
default: String,
|
|
276
|
+
): String = runCatching { if (hasKey(map, key)) map?.getString(key) ?: default else default }.getOrDefault(default)
|
|
277
|
+
|
|
278
|
+
private fun getInt(
|
|
279
|
+
map: ReadableMap?,
|
|
280
|
+
key: String,
|
|
281
|
+
default: Int,
|
|
282
|
+
): Int = runCatching { if (hasKey(map, key)) map?.getInt(key) ?: default else default }.getOrDefault(default)
|
|
283
|
+
|
|
284
|
+
private fun getDoubleOrNull(
|
|
285
|
+
map: ReadableMap?,
|
|
286
|
+
key: String,
|
|
287
|
+
): Double? = runCatching { if (hasKey(map, key)) map?.getDouble(key) else null }.getOrNull()
|
|
288
|
+
|
|
289
|
+
private fun isReactNativeFatalJsError(event: PostHogEvent): Boolean {
|
|
290
|
+
if (event.event != "\$exception") return false
|
|
291
|
+
val exceptionList = event.properties?.get("\$exception_list") as? List<*> ?: return false
|
|
292
|
+
return exceptionList.any { item ->
|
|
293
|
+
val exception = item as? Map<*, *> ?: return@any false
|
|
294
|
+
exception["type"] == "JavascriptException" &&
|
|
295
|
+
(exception["module"] as? String)?.startsWith("com.facebook.react") == true
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private fun logError(
|
|
300
|
+
method: String,
|
|
301
|
+
error: Throwable,
|
|
302
|
+
) {
|
|
303
|
+
Log.println(Log.ERROR, POSTHOG_TAG, "Method $method, error: $error")
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
companion object {
|
|
307
|
+
const val NAME = "PosthogReactNativePlugin"
|
|
308
|
+
const val POSTHOG_TAG = "PostHog"
|
|
309
|
+
|
|
310
|
+
// Default session replay configuration values
|
|
311
|
+
const val DEFAULT_MASK_ALL_TEXT_INPUTS = true
|
|
312
|
+
const val DEFAULT_MASK_ALL_IMAGES = true
|
|
313
|
+
const val DEFAULT_CAPTURE_LOG = true
|
|
314
|
+
const val DEFAULT_FLUSH_AT = 20
|
|
315
|
+
const val DEFAULT_THROTTLE_DELAY_MS = 1000
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package com.posthogreactnativeplugin
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class PosthogReactNativePluginPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
|
|
10
|
+
listOf(PosthogReactNativePluginModule(reactContext))
|
|
11
|
+
|
|
12
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
@interface RCT_EXTERN_MODULE(PosthogReactNativePlugin, NSObject)
|
|
4
|
+
|
|
5
|
+
RCT_EXTERN_METHOD(setup:(NSString)sessionId
|
|
6
|
+
withSdkOptions:(NSDictionary)sdkOptions
|
|
7
|
+
withPluginConfig:(NSDictionary)pluginConfig
|
|
8
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
9
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
|
|
11
|
+
RCT_EXTERN_METHOD(start:(NSString)sessionId
|
|
12
|
+
withSdkOptions:(NSDictionary)sdkOptions
|
|
13
|
+
withSdkReplayConfig:(NSDictionary)sdkReplayConfig
|
|
14
|
+
withDecideReplayConfig:(NSDictionary)decideReplayConfig
|
|
15
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
16
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
17
|
+
|
|
18
|
+
RCT_EXTERN_METHOD(startSession:(NSString)sessionId
|
|
19
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
20
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
21
|
+
|
|
22
|
+
RCT_EXTERN_METHOD(isEnabled:(RCTPromiseResolveBlock)resolve
|
|
23
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
24
|
+
|
|
25
|
+
RCT_EXTERN_METHOD(endSession:(RCTPromiseResolveBlock)resolve
|
|
26
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
27
|
+
|
|
28
|
+
RCT_EXTERN_METHOD(identify:(NSString)distinctId
|
|
29
|
+
withAnonymousId:(NSString)anonymousId
|
|
30
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
31
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
32
|
+
|
|
33
|
+
RCT_EXTERN_METHOD(startRecording:(BOOL)resumeCurrent
|
|
34
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
35
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
36
|
+
|
|
37
|
+
RCT_EXTERN_METHOD(stopRecording:(RCTPromiseResolveBlock)resolve
|
|
38
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
39
|
+
|
|
40
|
+
+ (BOOL)requiresMainQueueSetup
|
|
41
|
+
{
|
|
42
|
+
return NO;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import PostHog
|
|
2
|
+
|
|
3
|
+
/// Meant for internally logging PostHog related things
|
|
4
|
+
private func hedgeLog(_ message: String) {
|
|
5
|
+
print("[PostHog] \(message)")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Deduplication works on Android (both architectures) and iOS (old architecture only).
|
|
9
|
+
// On the iOS new architecture, fatal JS exception events surface as a generic SIGABRT
|
|
10
|
+
// crash event with no JS-error text in any field, so they currently cannot be filtered.
|
|
11
|
+
private let fatalJsErrorMarkers = ["Unhandled JS Exception", "ExceptionsManager.reportException", "facebook::jsi::JSError"]
|
|
12
|
+
|
|
13
|
+
private func containsFatalJsErrorMarker(_ text: String?) -> Bool {
|
|
14
|
+
guard let text else { return false }
|
|
15
|
+
return fatalJsErrorMarkers.contains { text.contains($0) }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private func isReactNativeFatalJsError(_ event: PostHogEvent) -> Bool {
|
|
19
|
+
guard event.event == "$exception",
|
|
20
|
+
let exceptionList = event.properties["$exception_list"] as? [[String: Any]]
|
|
21
|
+
else { return false }
|
|
22
|
+
return exceptionList.contains { exception in
|
|
23
|
+
if containsFatalJsErrorMarker(exception["type"] as? String) {
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
if containsFatalJsErrorMarker(exception["value"] as? String) {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
// New-architecture RN rethrows fatal JS errors as a C++ jsi::JSError (SIGABRT);
|
|
30
|
+
// the JS-error text only survives in the signal's crash-info message.
|
|
31
|
+
let mechanism = exception["mechanism"] as? [String: Any]
|
|
32
|
+
let meta = mechanism?["meta"] as? [String: Any]
|
|
33
|
+
let signal = meta?["signal"] as? [String: Any]
|
|
34
|
+
return containsFatalJsErrorMarker(signal?["crash_info_message"] as? String)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@objc(PosthogReactNativePlugin)
|
|
39
|
+
class PosthogReactNativePlugin: NSObject {
|
|
40
|
+
private var config: PostHogConfig?
|
|
41
|
+
|
|
42
|
+
@objc(setup:withSdkOptions:withPluginConfig:withResolver:withRejecter:)
|
|
43
|
+
func setup(
|
|
44
|
+
sessionId: String, sdkOptions: [String: Any], pluginConfig: [String: Any],
|
|
45
|
+
resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock
|
|
46
|
+
) {
|
|
47
|
+
let sessionReplayConfig = pluginConfig["sessionReplay"] as? [String: Any] ?? [:]
|
|
48
|
+
let errorTrackingConfig = pluginConfig["errorTracking"] as? [String: Any] ?? [:]
|
|
49
|
+
|
|
50
|
+
setupNativeSdk(
|
|
51
|
+
method: "setup",
|
|
52
|
+
sessionId: sessionId,
|
|
53
|
+
sdkOptions: sdkOptions,
|
|
54
|
+
sessionReplayEnabled: sessionReplayConfig["enabled"] as? Bool ?? false,
|
|
55
|
+
sdkReplayConfig: sessionReplayConfig["sdkReplayConfig"] as? [String: Any] ?? [:],
|
|
56
|
+
decideReplayConfig: sessionReplayConfig["decideReplayConfig"] as? [String: Any] ?? [:],
|
|
57
|
+
nativeErrorTrackingAutocapture: errorTrackingConfig["nativeAutocapture"] as? Bool ?? false,
|
|
58
|
+
resolve: resolve
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@objc(start:withSdkOptions:withSdkReplayConfig:withDecideReplayConfig:withResolver:withRejecter:)
|
|
63
|
+
func start(
|
|
64
|
+
sessionId: String, sdkOptions: [String: Any], sdkReplayConfig: [String: Any],
|
|
65
|
+
decideReplayConfig: [String: Any], resolve: RCTPromiseResolveBlock,
|
|
66
|
+
reject _: RCTPromiseRejectBlock
|
|
67
|
+
) {
|
|
68
|
+
setupNativeSdk(
|
|
69
|
+
method: "start",
|
|
70
|
+
sessionId: sessionId,
|
|
71
|
+
sdkOptions: sdkOptions,
|
|
72
|
+
sessionReplayEnabled: true,
|
|
73
|
+
sdkReplayConfig: sdkReplayConfig,
|
|
74
|
+
decideReplayConfig: decideReplayConfig,
|
|
75
|
+
nativeErrorTrackingAutocapture: false,
|
|
76
|
+
resolve: resolve
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private func setupNativeSdk(
|
|
81
|
+
method _: String,
|
|
82
|
+
sessionId: String,
|
|
83
|
+
sdkOptions: [String: Any],
|
|
84
|
+
sessionReplayEnabled: Bool,
|
|
85
|
+
sdkReplayConfig: [String: Any],
|
|
86
|
+
decideReplayConfig: [String: Any],
|
|
87
|
+
nativeErrorTrackingAutocapture: Bool,
|
|
88
|
+
resolve: RCTPromiseResolveBlock
|
|
89
|
+
) {
|
|
90
|
+
if sessionId.isEmpty {
|
|
91
|
+
hedgeLog("Invalid empty sessionId provided.")
|
|
92
|
+
resolve(nil)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let projectToken =
|
|
97
|
+
(sdkOptions["projectToken"] as? String)
|
|
98
|
+
?? (sdkOptions["apiKey"] as? String)
|
|
99
|
+
?? ""
|
|
100
|
+
let host = sdkOptions["host"] as? String ?? PostHogConfig.defaultHost
|
|
101
|
+
let debug = sdkOptions["debug"] as? Bool ?? false
|
|
102
|
+
|
|
103
|
+
PostHogSessionManager.shared.setSessionId(sessionId)
|
|
104
|
+
|
|
105
|
+
let config = PostHogConfig(projectToken: projectToken, host: host)
|
|
106
|
+
config.captureApplicationLifecycleEvents = false
|
|
107
|
+
config.captureScreenViews = false
|
|
108
|
+
config.debug = debug
|
|
109
|
+
config.errorTrackingConfig.autoCapture = nativeErrorTrackingAutocapture
|
|
110
|
+
|
|
111
|
+
// React Native rethrows fatal JS errors natively (RCTFatalException / ExceptionsManager).
|
|
112
|
+
// The JS layer already captured them, so drop the native duplicate.
|
|
113
|
+
config.setBeforeSend { event in
|
|
114
|
+
isReactNativeFatalJsError(event) ? nil : event
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if #available(iOS 15.0, *) {
|
|
118
|
+
config.surveys = false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Always apply the session replay configuration so that recording started later
|
|
122
|
+
// (e.g. startRecording or a linked feature flag) uses the right mode and masking;
|
|
123
|
+
// sessionReplayEnabled only controls whether recording starts at setup.
|
|
124
|
+
config.sessionReplay = sessionReplayEnabled
|
|
125
|
+
config.sessionReplayConfig.screenshotMode = true
|
|
126
|
+
|
|
127
|
+
let maskAllTextInputs = sdkReplayConfig["maskAllTextInputs"] as? Bool ?? true
|
|
128
|
+
config.sessionReplayConfig.maskAllTextInputs = maskAllTextInputs
|
|
129
|
+
|
|
130
|
+
let maskAllImages = sdkReplayConfig["maskAllImages"] as? Bool ?? true
|
|
131
|
+
config.sessionReplayConfig.maskAllImages = maskAllImages
|
|
132
|
+
|
|
133
|
+
let maskAllSandboxedViews = sdkReplayConfig["maskAllSandboxedViews"] as? Bool ?? true
|
|
134
|
+
config.sessionReplayConfig.maskAllSandboxedViews = maskAllSandboxedViews
|
|
135
|
+
|
|
136
|
+
// read throttleDelayMs and use iOSdebouncerDelayMs as a fallback for back compatibility
|
|
137
|
+
let throttleDelayMs =
|
|
138
|
+
(sdkReplayConfig["throttleDelayMs"] as? Int)
|
|
139
|
+
?? (sdkReplayConfig["iOSdebouncerDelayMs"] as? Int)
|
|
140
|
+
?? 1000
|
|
141
|
+
|
|
142
|
+
let timeInterval: TimeInterval = Double(throttleDelayMs) / 1000.0
|
|
143
|
+
config.sessionReplayConfig.throttleDelay = timeInterval
|
|
144
|
+
|
|
145
|
+
let captureNetworkTelemetry = sdkReplayConfig["captureNetworkTelemetry"] as? Bool ?? true
|
|
146
|
+
config.sessionReplayConfig.captureNetworkTelemetry = captureNetworkTelemetry
|
|
147
|
+
|
|
148
|
+
let captureLog = sdkReplayConfig["captureLog"] as? Bool ?? true
|
|
149
|
+
config.sessionReplayConfig.captureLogs = captureLog
|
|
150
|
+
|
|
151
|
+
config.sessionReplayConfig.sampleRate = sdkReplayConfig["sampleRate"] as? NSNumber
|
|
152
|
+
|
|
153
|
+
let screenshotModeBackgroundCapture = sdkReplayConfig["screenshotModeBackgroundCapture"] as? Bool ?? false
|
|
154
|
+
config.sessionReplayConfig.screenshotModeBackgroundCapture = screenshotModeBackgroundCapture
|
|
155
|
+
|
|
156
|
+
let endpoint = decideReplayConfig["endpoint"] as? String ?? ""
|
|
157
|
+
if !endpoint.isEmpty {
|
|
158
|
+
config.snapshotEndpoint = endpoint
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let distinctId = sdkOptions["distinctId"] as? String ?? ""
|
|
162
|
+
let anonymousId = sdkOptions["anonymousId"] as? String ?? ""
|
|
163
|
+
|
|
164
|
+
let sdkVersion = sdkOptions["sdkVersion"] as? String ?? ""
|
|
165
|
+
|
|
166
|
+
let flushAt = sdkOptions["flushAt"] as? Int ?? 20
|
|
167
|
+
config.flushAt = flushAt
|
|
168
|
+
|
|
169
|
+
if !sdkVersion.isEmpty {
|
|
170
|
+
postHogSdkName = "posthog-react-native"
|
|
171
|
+
postHogVersion = sdkVersion
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
PostHogSDK.shared.setup(config)
|
|
175
|
+
|
|
176
|
+
self.config = config
|
|
177
|
+
|
|
178
|
+
guard let storageManager = self.config?.storageManager else {
|
|
179
|
+
hedgeLog("Storage manager is not available in the config.")
|
|
180
|
+
resolve(nil)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
setIdentify(storageManager, distinctId: distinctId, anonymousId: anonymousId)
|
|
185
|
+
|
|
186
|
+
resolve(nil)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@objc(startSession:withResolver:withRejecter:)
|
|
190
|
+
func startSession(
|
|
191
|
+
sessionId: String, resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock
|
|
192
|
+
) {
|
|
193
|
+
if sessionId.isEmpty {
|
|
194
|
+
hedgeLog("Invalid empty sessionId provided.")
|
|
195
|
+
resolve(nil)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
PostHogSessionManager.shared.setSessionId(sessionId)
|
|
199
|
+
PostHogSDK.shared.startSession()
|
|
200
|
+
resolve(nil)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@objc(isEnabled:withRejecter:)
|
|
204
|
+
func isEnabled(resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock) {
|
|
205
|
+
let isEnabled = PostHogSDK.shared.isSessionReplayActive()
|
|
206
|
+
resolve(isEnabled)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@objc(endSession:withRejecter:)
|
|
210
|
+
func endSession(resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock) {
|
|
211
|
+
PostHogSDK.shared.endSession()
|
|
212
|
+
resolve(nil)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@objc(identify:withAnonymousId:withResolver:withRejecter:)
|
|
216
|
+
func identify(
|
|
217
|
+
distinctId: String, anonymousId: String, resolve: RCTPromiseResolveBlock,
|
|
218
|
+
reject _: RCTPromiseRejectBlock
|
|
219
|
+
) {
|
|
220
|
+
guard let storageManager = config?.storageManager else {
|
|
221
|
+
hedgeLog("Storage manager is not available in the config.")
|
|
222
|
+
resolve(nil)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
setIdentify(storageManager, distinctId: distinctId, anonymousId: anonymousId)
|
|
226
|
+
|
|
227
|
+
resolve(nil)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private func setIdentify(
|
|
231
|
+
_ storageManager: PostHogStorageManager, distinctId: String, anonymousId: String
|
|
232
|
+
) {
|
|
233
|
+
if !anonymousId.isEmpty {
|
|
234
|
+
storageManager.setAnonymousId(anonymousId)
|
|
235
|
+
}
|
|
236
|
+
if !distinctId.isEmpty {
|
|
237
|
+
storageManager.setDistinctId(distinctId)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@objc(startRecording:withResolver:withRejecter:)
|
|
242
|
+
func startRecording(
|
|
243
|
+
resumeCurrent: Bool, resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock
|
|
244
|
+
) {
|
|
245
|
+
PostHogSDK.shared.startSessionRecording(resumeCurrent: resumeCurrent)
|
|
246
|
+
resolve(nil)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@objc(stopRecording:withRejecter:)
|
|
250
|
+
func stopRecording(resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock) {
|
|
251
|
+
PostHogSDK.shared.stopSessionRecording()
|
|
252
|
+
resolve(nil)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
exports.endSession = endSession;
|
|
8
|
+
exports.identify = identify;
|
|
9
|
+
exports.isEnabled = isEnabled;
|
|
10
|
+
exports.setup = setup;
|
|
11
|
+
exports.start = start;
|
|
12
|
+
exports.startRecording = startRecording;
|
|
13
|
+
exports.startSession = startSession;
|
|
14
|
+
exports.stopRecording = stopRecording;
|
|
15
|
+
var _reactNative = require("react-native");
|
|
16
|
+
const LINKING_ERROR = `The package '@posthog/react-native-plugin' doesn't seem to be linked. Make sure: \n\n` + _reactNative.Platform.select({
|
|
17
|
+
ios: "- You have run 'pod install'\n",
|
|
18
|
+
default: ''
|
|
19
|
+
}) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n';
|
|
20
|
+
const PosthogReactNativePlugin = _reactNative.NativeModules.PosthogReactNativePlugin ? _reactNative.NativeModules.PosthogReactNativePlugin : new Proxy({}, {
|
|
21
|
+
get() {
|
|
22
|
+
throw new Error(LINKING_ERROR);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
function setup(sessionId, sdkOptions, pluginConfig = {}) {
|
|
26
|
+
return PosthogReactNativePlugin.setup(sessionId, sdkOptions, pluginConfig);
|
|
27
|
+
}
|
|
28
|
+
function start(sessionId, sdkOptions, sdkReplayConfig, decideReplayConfig) {
|
|
29
|
+
return PosthogReactNativePlugin.start(sessionId, sdkOptions, sdkReplayConfig, decideReplayConfig);
|
|
30
|
+
}
|
|
31
|
+
function startSession(sessionId) {
|
|
32
|
+
return PosthogReactNativePlugin.startSession(sessionId);
|
|
33
|
+
}
|
|
34
|
+
function endSession() {
|
|
35
|
+
return PosthogReactNativePlugin.endSession();
|
|
36
|
+
}
|
|
37
|
+
function isEnabled() {
|
|
38
|
+
return PosthogReactNativePlugin.isEnabled();
|
|
39
|
+
}
|
|
40
|
+
function identify(distinctId, anonymousId) {
|
|
41
|
+
return PosthogReactNativePlugin.identify(distinctId, anonymousId);
|
|
42
|
+
}
|
|
43
|
+
function startRecording(resumeCurrent) {
|
|
44
|
+
return PosthogReactNativePlugin.startRecording(resumeCurrent);
|
|
45
|
+
}
|
|
46
|
+
function stopRecording() {
|
|
47
|
+
return PosthogReactNativePlugin.stopRecording();
|
|
48
|
+
}
|
|
49
|
+
const PostHogReactNativePlugin = {
|
|
50
|
+
setup,
|
|
51
|
+
start,
|
|
52
|
+
startSession,
|
|
53
|
+
endSession,
|
|
54
|
+
isEnabled,
|
|
55
|
+
identify,
|
|
56
|
+
startRecording,
|
|
57
|
+
stopRecording
|
|
58
|
+
};
|
|
59
|
+
var _default = exports.default = PostHogReactNativePlugin;
|
|
60
|
+
//# sourceMappingURL=index.js.map
|