@plushanalytics/react-native-session-replay 1.2.6 → 3.0.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.
Files changed (59) hide show
  1. package/README.md +138 -3
  2. package/android/src/main/java/com/posthogreactnativesessionreplay/PosthogReactNativeSessionReplayModule.kt +68 -27
  3. package/ios/PosthogReactNativeSessionReplay.mm +3 -0
  4. package/ios/PosthogReactNativeSessionReplay.swift +58 -31
  5. package/lib/commonjs/adapters/mobileReplayAdapter.js +135 -0
  6. package/lib/commonjs/adapters/mobileReplayAdapter.js.map +1 -0
  7. package/lib/commonjs/client.js +42 -0
  8. package/lib/commonjs/client.js.map +1 -0
  9. package/lib/commonjs/index.js +31 -37
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/nativeBridge.js +50 -0
  12. package/lib/commonjs/nativeBridge.js.map +1 -0
  13. package/lib/commonjs/provider.js +45 -0
  14. package/lib/commonjs/provider.js.map +1 -0
  15. package/lib/commonjs/types.js +6 -0
  16. package/lib/commonjs/types.js.map +1 -0
  17. package/lib/module/adapters/mobileReplayAdapter.js +130 -0
  18. package/lib/module/adapters/mobileReplayAdapter.js.map +1 -0
  19. package/lib/module/client.js +33 -0
  20. package/lib/module/client.js.map +1 -0
  21. package/lib/module/index.js +3 -33
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/nativeBridge.js +40 -0
  24. package/lib/module/nativeBridge.js.map +1 -0
  25. package/lib/module/provider.js +39 -0
  26. package/lib/module/provider.js.map +1 -0
  27. package/lib/module/types.js +4 -0
  28. package/lib/module/types.js.map +1 -0
  29. package/lib/typescript/commonjs/src/adapters/mobileReplayAdapter.d.ts +9 -0
  30. package/lib/typescript/commonjs/src/adapters/mobileReplayAdapter.d.ts.map +1 -0
  31. package/lib/typescript/commonjs/src/client.d.ts +6 -0
  32. package/lib/typescript/commonjs/src/client.d.ts.map +1 -0
  33. package/lib/typescript/commonjs/src/index.d.ts +4 -28
  34. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/src/nativeBridge.d.ts +31 -0
  36. package/lib/typescript/commonjs/src/nativeBridge.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/src/provider.d.ts +12 -0
  38. package/lib/typescript/commonjs/src/provider.d.ts.map +1 -0
  39. package/lib/typescript/commonjs/src/types.d.ts +32 -0
  40. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  41. package/lib/typescript/module/src/adapters/mobileReplayAdapter.d.ts +9 -0
  42. package/lib/typescript/module/src/adapters/mobileReplayAdapter.d.ts.map +1 -0
  43. package/lib/typescript/module/src/client.d.ts +6 -0
  44. package/lib/typescript/module/src/client.d.ts.map +1 -0
  45. package/lib/typescript/module/src/index.d.ts +4 -28
  46. package/lib/typescript/module/src/index.d.ts.map +1 -1
  47. package/lib/typescript/module/src/nativeBridge.d.ts +31 -0
  48. package/lib/typescript/module/src/nativeBridge.d.ts.map +1 -0
  49. package/lib/typescript/module/src/provider.d.ts +12 -0
  50. package/lib/typescript/module/src/provider.d.ts.map +1 -0
  51. package/lib/typescript/module/src/types.d.ts +32 -0
  52. package/lib/typescript/module/src/types.d.ts.map +1 -0
  53. package/package.json +22 -25
  54. package/src/adapters/mobileReplayAdapter.ts +206 -0
  55. package/src/client.ts +43 -0
  56. package/src/index.tsx +25 -79
  57. package/src/nativeBridge.ts +86 -0
  58. package/src/provider.tsx +46 -0
  59. package/src/types.ts +73 -0
package/README.md CHANGED
@@ -1,9 +1,144 @@
1
1
  # @plushanalytics/react-native-session-replay
2
2
 
3
- Session Replay for React Native (Android and iOS)
3
+ React Native SDK for Plush Analytics (events + session replay) with a React Provider/hook.
4
4
 
5
- ## Installation
5
+ ## Install
6
6
 
7
- ```sh
7
+ ```bash
8
8
  npm install @plushanalytics/react-native-session-replay
9
9
  ```
10
+
11
+ > [!NOTE]
12
+ > This package includes native code and is not compatible with Expo Go. Use development builds:
13
+
14
+ ```bash
15
+ npx expo run:ios
16
+ npx expo run:android
17
+ ```
18
+
19
+ ## Quick Setup (Provider + Hook)
20
+
21
+ ```tsx
22
+ import React from "react";
23
+ import { Button } from "react-native";
24
+ import { PlushAnalyticsProvider, usePlushAnalytics } from "@plushanalytics/react-native-session-replay";
25
+
26
+ function CheckoutButton(): JSX.Element {
27
+ const analytics = usePlushAnalytics();
28
+ return (
29
+ <Button
30
+ title="Purchase"
31
+ onPress={() => {
32
+ void analytics.track("purchase_clicked", { properties: { plan: "pro" } });
33
+ }}
34
+ />
35
+ );
36
+ }
37
+
38
+ export default function App(): JSX.Element {
39
+ return (
40
+ <PlushAnalyticsProvider
41
+ config={{
42
+ apiKey: "plsh_live_...",
43
+ projectId: "mobile_app",
44
+ context: { platform: "react-native" },
45
+ sessionReplayConfig: {
46
+ enabled: true,
47
+ masking: {
48
+ textInputs: true,
49
+ images: true,
50
+ sandboxedViews: true,
51
+ },
52
+ captureNetworkTelemetry: true,
53
+ throttleDelayMs: 1000,
54
+ },
55
+ }}
56
+ >
57
+ <CheckoutButton />
58
+ </PlushAnalyticsProvider>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Direct Client Usage (No React)
64
+
65
+ ```ts
66
+ import { createPlushAnalyticsClient } from "@plushanalytics/react-native-session-replay";
67
+
68
+ const analytics = createPlushAnalyticsClient({
69
+ apiKey: "plsh_live_...",
70
+ projectId: "mobile_app",
71
+ sessionReplayConfig: {
72
+ enabled: true,
73
+ masking: {
74
+ textInputs: true,
75
+ images: true,
76
+ sandboxedViews: true,
77
+ },
78
+ },
79
+ });
80
+
81
+ await analytics.track("app_opened");
82
+ await analytics.shutdown();
83
+ ```
84
+
85
+ ## Session Replay Config
86
+
87
+ Replay behavior is configured locally in the SDK. It does not depend on backend remote config.
88
+
89
+ - `config.sessionReplayConfig.enabled`: defaults to `true`
90
+ - `config.sessionReplayConfig.masking.textInputs`: defaults to `true`
91
+ - `config.sessionReplayConfig.masking.images`: defaults to `true`
92
+ - `config.sessionReplayConfig.masking.sandboxedViews`: defaults to `true`
93
+ - `config.sessionReplayConfig.captureLog`: defaults to `true`
94
+ - `config.sessionReplayConfig.captureNetworkTelemetry`: defaults to `true`
95
+ - `config.sessionReplayConfig.throttleDelayMs`: defaults to `1000`
96
+
97
+ Per-session overrides are supported via `startSessionRecording({ replayConfig })` / `startReplay({ replayConfig })`.
98
+
99
+ ```ts
100
+ await analytics.startReplay({
101
+ replayConfig: {
102
+ enabled: true,
103
+ masking: {
104
+ images: false,
105
+ },
106
+ },
107
+ });
108
+ ```
109
+
110
+ Legacy flat replay keys are still supported in per-session overrides for backward compatibility:
111
+ `maskAllTextInputs`, `maskAllImages`, `maskAllSandboxedViews`.
112
+
113
+ ## Replay Authentication Headers
114
+
115
+ Replay uploads include both headers from the configured `apiKey`:
116
+
117
+ - `x-api-key`
118
+ - `x-plush-api-key`
119
+
120
+ ## Common Usage
121
+
122
+ ```ts
123
+ await analytics.track("cta_clicked", { properties: { plan: "pro" } });
124
+ await analytics.identify("user_123", { traits: { plan: "pro" } });
125
+ await analytics.screen("Settings", { properties: { tab: "billing" } });
126
+ await analytics.group("org_456", { traits: { tier: "enterprise" } });
127
+
128
+ await analytics.flush();
129
+ await analytics.reset();
130
+
131
+ await analytics.startSessionRecording();
132
+ await analytics.flushSessionRecording();
133
+ await analytics.stopSessionRecording();
134
+
135
+ analytics.setProject("mobile_app");
136
+ analytics.setContext({ build: 42 });
137
+ ```
138
+
139
+ ## API Surface
140
+
141
+ - `PlushAnalyticsProvider`
142
+ - `usePlushAnalytics()`
143
+ - `createPlushAnalyticsClient(config)`
144
+ - `PlushAnalyticsClient`
@@ -15,7 +15,10 @@ import com.posthog.internal.PostHogPreferences
15
15
  import com.posthog.internal.PostHogPreferences.Companion.ANONYMOUS_ID
16
16
  import com.posthog.internal.PostHogPreferences.Companion.DISTINCT_ID
17
17
  import com.posthog.internal.PostHogSessionManager
18
+ import java.net.URI
18
19
  import java.util.UUID
20
+ import okhttp3.Interceptor
21
+ import okhttp3.OkHttpClient
19
22
 
20
23
  class PosthogReactNativeSessionReplayModule(
21
24
  reactContext: ReactApplicationContext,
@@ -39,13 +42,24 @@ class PosthogReactNativeSessionReplayModule(
39
42
  val context = this.reactApplicationContext
40
43
  val apiKey = runCatching { sdkOptions.getString("apiKey") }.getOrNull() ?: ""
41
44
  val host = runCatching { sdkOptions.getString("host") }.getOrNull() ?: PostHogConfig.DEFAULT_HOST
45
+ if (isPosthogHost(host)) {
46
+ val message = "Blocked replay host '$host' because posthog.com hosts are not allowed."
47
+ logError("start", IllegalArgumentException(message))
48
+ promise.reject("PLUSH_BLOCKED_REPLAY_HOST", message)
49
+ return@Runnable
50
+ }
51
+
42
52
  val debugValue = runCatching { sdkOptions.getBoolean("debug") }.getOrNull() ?: false
43
53
 
44
- val maskAllTextInputs = runCatching { sdkReplayConfig.getBoolean("maskAllTextInputs") }.getOrNull() ?: DEFAULT_MASK_ALL_TEXT_INPUTS
45
- val maskAllImages = runCatching { sdkReplayConfig.getBoolean("maskAllImages") }.getOrNull() ?: DEFAULT_MASK_ALL_IMAGES
46
- val captureLog = runCatching { sdkReplayConfig.getBoolean("captureLog") }.getOrNull() ?: DEFAULT_CAPTURE_LOG
54
+ val maskAllTextInputs =
55
+ runCatching { sdkReplayConfig.getBoolean("maskAllTextInputs") }.getOrNull()
56
+ ?: DEFAULT_MASK_ALL_TEXT_INPUTS
57
+ val maskAllImages =
58
+ runCatching { sdkReplayConfig.getBoolean("maskAllImages") }.getOrNull() ?: DEFAULT_MASK_ALL_IMAGES
59
+ val captureLog =
60
+ runCatching { sdkReplayConfig.getBoolean("captureLog") }.getOrNull() ?: DEFAULT_CAPTURE_LOG
47
61
 
48
- // read throttleDelayMs and use androidDebouncerDelayMs as a fallback for back compatibility
62
+ // Read throttleDelayMs and use androidDebouncerDelayMs as a fallback for back compatibility.
49
63
  val throttleDelayMs =
50
64
  if (sdkReplayConfig.hasKey("throttleDelayMs")) {
51
65
  sdkReplayConfig.getInt("throttleDelayMs")
@@ -56,24 +70,10 @@ class PosthogReactNativeSessionReplayModule(
56
70
  }
57
71
 
58
72
  val endpoint = decideReplayConfig.getString("endpoint")
59
-
60
- val distinctId =
61
- try {
62
- sdkOptions.getString("distinctId") ?: ""
63
- } catch (e: Throwable) {
64
- logError("parse distinctId", e)
65
- ""
66
- }
67
- val anonymousId =
68
- try {
69
- sdkOptions.getString("anonymousId") ?: ""
70
- } catch (e: Throwable) {
71
- logError("parse anonymousId", e)
72
- ""
73
- }
74
- val theSdkVersion = runCatching { sdkOptions.getString("sdkVersion") }.getOrNull()
75
-
76
- val theFlushAt = runCatching { sdkOptions.getInt("flushAt") }.getOrNull() ?: DEFAULT_FLUSH_AT
73
+ val distinctId = runCatching { sdkOptions.getString("distinctId") }.getOrNull() ?: ""
74
+ val anonymousId = runCatching { sdkOptions.getString("anonymousId") }.getOrNull() ?: ""
75
+ val sdkVersion = runCatching { sdkOptions.getString("sdkVersion") }.getOrNull()
76
+ val flushAt = runCatching { sdkOptions.getInt("flushAt") }.getOrNull() ?: DEFAULT_FLUSH_AT
77
77
 
78
78
  val config =
79
79
  PostHogAndroidConfig(apiKey, host).apply {
@@ -81,25 +81,28 @@ class PosthogReactNativeSessionReplayModule(
81
81
  captureDeepLinks = false
82
82
  captureApplicationLifecycleEvents = false
83
83
  captureScreenViews = false
84
- flushAt = theFlushAt
84
+ preloadFeatureFlags = false
85
+ remoteConfig = false
86
+ this.flushAt = flushAt
85
87
  sessionReplay = true
86
88
  sessionReplayConfig.screenshot = true
87
89
  sessionReplayConfig.captureLogcat = captureLog
88
90
  sessionReplayConfig.throttleDelayMs = throttleDelayMs.toLong()
89
91
  sessionReplayConfig.maskAllImages = maskAllImages
90
92
  sessionReplayConfig.maskAllTextInputs = maskAllTextInputs
93
+ httpClient = buildApiKeyClient(apiKey)
91
94
 
92
95
  if (!endpoint.isNullOrEmpty()) {
93
96
  snapshotEndpoint = endpoint
94
97
  }
95
98
 
96
- if (!theSdkVersion.isNullOrEmpty()) {
99
+ if (!sdkVersion.isNullOrEmpty()) {
97
100
  sdkName = "posthog-react-native"
98
- sdkVersion = theSdkVersion
101
+ this.sdkVersion = sdkVersion
99
102
  }
100
103
  }
101
- PostHogAndroid.setup(context, config)
102
104
 
105
+ PostHogAndroid.setup(context, config)
103
106
  setIdentify(config.cachePreferences, distinctId, anonymousId)
104
107
  } catch (e: Throwable) {
105
108
  logError("start", e)
@@ -108,7 +111,7 @@ class PosthogReactNativeSessionReplayModule(
108
111
  }
109
112
  }
110
113
 
111
- // forces the SDK to be initialized on the main thread
114
+ // Force the SDK to be initialized on the main thread.
112
115
  if (UiThreadUtil.isOnUiThread()) {
113
116
  initRunnable.run()
114
117
  } else {
@@ -153,6 +156,17 @@ class PosthogReactNativeSessionReplayModule(
153
156
  }
154
157
  }
155
158
 
159
+ @ReactMethod
160
+ fun flush(promise: Promise) {
161
+ try {
162
+ PostHog.flush()
163
+ } catch (e: Throwable) {
164
+ logError("flush", e)
165
+ } finally {
166
+ promise.resolve(null)
167
+ }
168
+ }
169
+
156
170
  @ReactMethod
157
171
  fun identify(
158
172
  distinctId: String,
@@ -183,6 +197,22 @@ class PosthogReactNativeSessionReplayModule(
183
197
  }
184
198
  }
185
199
 
200
+ private fun buildApiKeyClient(apiKey: String): OkHttpClient {
201
+ return OkHttpClient.Builder()
202
+ .addInterceptor(
203
+ Interceptor { chain ->
204
+ val original = chain.request()
205
+ val updated =
206
+ original
207
+ .newBuilder()
208
+ .header("x-api-key", apiKey)
209
+ .header("x-plush-api-key", apiKey)
210
+ .build()
211
+ chain.proceed(updated)
212
+ }
213
+ ).build()
214
+ }
215
+
186
216
  private fun logError(
187
217
  method: String,
188
218
  error: Throwable,
@@ -190,6 +220,17 @@ class PosthogReactNativeSessionReplayModule(
190
220
  Log.println(Log.ERROR, POSTHOG_TAG, "Method $method, error: $error")
191
221
  }
192
222
 
223
+ private fun isPosthogHost(value: String): Boolean {
224
+ val hostName =
225
+ try {
226
+ URI(value).host?.lowercase() ?: value.lowercase()
227
+ } catch (_: Throwable) {
228
+ value.lowercase()
229
+ }
230
+
231
+ return hostName == "posthog.com" || hostName.endsWith(".posthog.com")
232
+ }
233
+
193
234
  companion object {
194
235
  const val NAME = "PosthogReactNativeSessionReplay"
195
236
  const val POSTHOG_TAG = "PostHog"
@@ -19,6 +19,9 @@ RCT_EXTERN_METHOD(isEnabled:(RCTPromiseResolveBlock)resolve
19
19
  RCT_EXTERN_METHOD(endSession:(RCTPromiseResolveBlock)resolve
20
20
  withRejecter:(RCTPromiseRejectBlock)reject)
21
21
 
22
+ RCT_EXTERN_METHOD(flush:(RCTPromiseResolveBlock)resolve
23
+ withRejecter:(RCTPromiseRejectBlock)reject)
24
+
22
25
  RCT_EXTERN_METHOD(identify:(NSString)distinctId
23
26
  withAnonymousId:(NSString)anonymousId
24
27
  withResolver:(RCTPromiseResolveBlock)resolve
@@ -1,8 +1,19 @@
1
+ import Foundation
1
2
  import PostHog
2
3
 
3
- // Meant for internally logging PostHog related things
4
- private func hedgeLog(_ message: String) {
5
- print("[PostHog] \(message)")
4
+ private func isPosthogHost(_ value: String) -> Bool {
5
+ let normalizedHost: String
6
+ if let url = URL(string: value), let host = url.host {
7
+ normalizedHost = host.lowercased()
8
+ } else {
9
+ normalizedHost = value.lowercased()
10
+ }
11
+
12
+ return normalizedHost == "posthog.com" || normalizedHost.hasSuffix(".posthog.com")
13
+ }
14
+
15
+ private func logNativeError(_ message: String) {
16
+ NSLog("[Plush][RNReplay] %@", message)
6
17
  }
7
18
 
8
19
  @objc(PosthogReactNativeSessionReplay)
@@ -11,13 +22,15 @@ class PosthogReactNativeSessionReplay: NSObject {
11
22
 
12
23
  @objc(start:withSdkOptions:withSdkReplayConfig:withDecideReplayConfig:withResolver:withRejecter:)
13
24
  func start(
14
- sessionId: String, sdkOptions: [String: Any], sdkReplayConfig: [String: Any],
15
- decideReplayConfig: [String: Any], resolve: RCTPromiseResolveBlock,
16
- reject _: RCTPromiseRejectBlock
25
+ sessionId: String,
26
+ sdkOptions: [String: Any],
27
+ sdkReplayConfig: [String: Any],
28
+ decideReplayConfig: [String: Any],
29
+ resolve: RCTPromiseResolveBlock,
30
+ reject: RCTPromiseRejectBlock
17
31
  ) {
18
- // we've seen cases where distinctId and anonymousId are not strings, so we need to check and convert them
19
32
  guard let sessionIdStr = sessionId as? String else {
20
- hedgeLog("Invalid sessionId provided: \(sessionId). Expected a string.")
33
+ logNativeError("Invalid sessionId provided. Expected a string.")
21
34
  resolve(nil)
22
35
  return
23
36
  }
@@ -26,15 +39,30 @@ class PosthogReactNativeSessionReplay: NSObject {
26
39
  let host = sdkOptions["host"] as? String ?? PostHogConfig.defaultHost
27
40
  let debug = sdkOptions["debug"] as? Bool ?? false
28
41
 
42
+ if isPosthogHost(host) {
43
+ let message = "Blocked replay host '\(host)' because posthog.com hosts are not allowed."
44
+ reject("PLUSH_BLOCKED_REPLAY_HOST", message, nil)
45
+ return
46
+ }
47
+
29
48
  PostHogSessionManager.shared.setSessionId(sessionIdStr)
30
49
 
31
50
  let config = PostHogConfig(apiKey: apiKey, host: host)
32
51
  config.sessionReplay = true
33
52
  config.captureApplicationLifecycleEvents = false
34
53
  config.captureScreenViews = false
54
+ config.preloadFeatureFlags = false
55
+ config.remoteConfig = false
35
56
  config.debug = debug
36
57
  config.sessionReplayConfig.screenshotMode = true
37
58
 
59
+ let sessionConfiguration = URLSessionConfiguration.default
60
+ sessionConfiguration.httpAdditionalHeaders = [
61
+ "x-api-key": apiKey,
62
+ "x-plush-api-key": apiKey,
63
+ ]
64
+ config.urlSessionConfiguration = sessionConfiguration
65
+
38
66
  if #available(iOS 15.0, *) {
39
67
  config.surveys = false
40
68
  }
@@ -48,7 +76,7 @@ class PosthogReactNativeSessionReplay: NSObject {
48
76
  let maskAllSandboxedViews = sdkReplayConfig["maskAllSandboxedViews"] as? Bool ?? true
49
77
  config.sessionReplayConfig.maskAllSandboxedViews = maskAllSandboxedViews
50
78
 
51
- // read throttleDelayMs and use iOSdebouncerDelayMs as a fallback for back compatibility
79
+ // Read throttleDelayMs and use iOSdebouncerDelayMs as a fallback for back compatibility.
52
80
  let throttleDelayMs =
53
81
  (sdkReplayConfig["throttleDelayMs"] as? Int)
54
82
  ?? (sdkReplayConfig["iOSdebouncerDelayMs"] as? Int)
@@ -79,30 +107,30 @@ class PosthogReactNativeSessionReplay: NSObject {
79
107
  }
80
108
 
81
109
  PostHogSDK.shared.setup(config)
82
-
83
110
  self.config = config
84
111
 
85
112
  guard let storageManager = self.config?.storageManager else {
86
- hedgeLog("Storage manager is not available in the config.")
113
+ logNativeError("Storage manager is not available in the config.")
87
114
  resolve(nil)
88
115
  return
89
116
  }
90
117
 
91
118
  setIdentify(storageManager, distinctId: distinctId, anonymousId: anonymousId)
92
-
93
119
  resolve(nil)
94
120
  }
95
121
 
96
122
  @objc(startSession:withResolver:withRejecter:)
97
123
  func startSession(
98
- sessionId: String, resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock
124
+ sessionId: String,
125
+ resolve: RCTPromiseResolveBlock,
126
+ reject _: RCTPromiseRejectBlock
99
127
  ) {
100
- // we've seen cases where distinctId and anonymousId are not strings, so we need to check and convert them
101
128
  guard let sessionIdStr = sessionId as? String else {
102
- hedgeLog("Invalid sessionId provided: \(sessionId). Expected a string.")
129
+ logNativeError("Invalid sessionId provided. Expected a string.")
103
130
  resolve(nil)
104
131
  return
105
132
  }
133
+
106
134
  PostHogSessionManager.shared.setSessionId(sessionIdStr)
107
135
  PostHogSDK.shared.startSession()
108
136
  resolve(nil)
@@ -110,8 +138,7 @@ class PosthogReactNativeSessionReplay: NSObject {
110
138
 
111
139
  @objc(isEnabled:withRejecter:)
112
140
  func isEnabled(resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock) {
113
- let isEnabled = PostHogSDK.shared.isSessionReplayActive()
114
- resolve(isEnabled)
141
+ resolve(PostHogSDK.shared.isSessionReplayActive())
115
142
  }
116
143
 
117
144
  @objc(endSession:withRejecter:)
@@ -120,33 +147,33 @@ class PosthogReactNativeSessionReplay: NSObject {
120
147
  resolve(nil)
121
148
  }
122
149
 
150
+ @objc(flush:withRejecter:)
151
+ func flush(resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock) {
152
+ PostHogSDK.shared.flush()
153
+ resolve(nil)
154
+ }
155
+
123
156
  @objc(identify:withAnonymousId:withResolver:withRejecter:)
124
157
  func identify(
125
- distinctId: String, anonymousId: String, resolve: RCTPromiseResolveBlock,
158
+ distinctId: String,
159
+ anonymousId: String,
160
+ resolve: RCTPromiseResolveBlock,
126
161
  reject _: RCTPromiseRejectBlock
127
162
  ) {
128
- // we've seen cases where distinctId and anonymousId are not strings, so we need to check and convert them
129
- guard let distinctIdStr = distinctId as? String,
130
- let anonymousIdStr = anonymousId as? String
131
- else {
132
- hedgeLog(
133
- "Invalid distinctId: \(distinctId) or anonymousId: \(anonymousId) provided. Expected strings."
134
- )
135
- resolve(nil)
136
- return
137
- }
138
163
  guard let storageManager = config?.storageManager else {
139
- hedgeLog("Storage manager is not available in the config.")
164
+ logNativeError("Storage manager is not available in the config.")
140
165
  resolve(nil)
141
166
  return
142
167
  }
143
- setIdentify(storageManager, distinctId: distinctIdStr, anonymousId: anonymousIdStr)
144
168
 
169
+ setIdentify(storageManager, distinctId: distinctId, anonymousId: anonymousId)
145
170
  resolve(nil)
146
171
  }
147
172
 
148
173
  private func setIdentify(
149
- _ storageManager: PostHogStorageManager, distinctId: String, anonymousId: String
174
+ _ storageManager: PostHogStorageManager,
175
+ distinctId: String,
176
+ anonymousId: String
150
177
  ) {
151
178
  if !anonymousId.isEmpty {
152
179
  storageManager.setAnonymousId(anonymousId)
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createMobileReplayRecorderAdapter = createMobileReplayRecorderAdapter;
7
+ var replayModule = _interopRequireWildcard(require("../nativeBridge.js"));
8
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
9
+ const DEFAULT_SNAPSHOT_PATH = "/v1/events/batch";
10
+ const DEFAULT_THROTTLE_DELAY_MS = 1000;
11
+ const UUID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
12
+ function assertNonEmpty(value, field) {
13
+ if (!value || !value.trim()) {
14
+ throw new Error(`${field} is required.`);
15
+ }
16
+ }
17
+ function normalizeHost(value) {
18
+ return value.replace(/\/+$/, "");
19
+ }
20
+ function normalizeSnapshotPath(value) {
21
+ const trimmed = value?.trim();
22
+ if (!trimmed) return DEFAULT_SNAPSHOT_PATH;
23
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
24
+ }
25
+ function isUuid(value) {
26
+ return UUID_PATTERN.test(value);
27
+ }
28
+ function generateUuid() {
29
+ const cryptoApi = globalThis.crypto;
30
+ if (cryptoApi?.randomUUID) return cryptoApi.randomUUID();
31
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, char => {
32
+ const random = Math.floor(Math.random() * 16);
33
+ const resolved = char === "x" ? random : random & 0x3 | 0x8;
34
+ return resolved.toString(16);
35
+ });
36
+ }
37
+ function resolveSessionId(options) {
38
+ const requestedSessionId = options?.sessionId?.trim();
39
+ if (requestedSessionId && isUuid(requestedSessionId)) {
40
+ return requestedSessionId;
41
+ }
42
+ return generateUuid();
43
+ }
44
+ function assertNoPosthogDotCom(host) {
45
+ const lowered = host.toLowerCase();
46
+ const hostName = (() => {
47
+ try {
48
+ return new URL(host).hostname.toLowerCase();
49
+ } catch {
50
+ return lowered;
51
+ }
52
+ })();
53
+ if (hostName === "posthog.com" || hostName.endsWith(".posthog.com")) {
54
+ throw new Error("replayHost must not point to posthog.com (or a subdomain).");
55
+ }
56
+ }
57
+ function resolveMaskingValue(overrideConfig, defaultConfig, maskingKey, legacyKey) {
58
+ const overrideMaskingValue = overrideConfig?.masking?.[maskingKey];
59
+ if (typeof overrideMaskingValue === "boolean") {
60
+ return overrideMaskingValue;
61
+ }
62
+ const overrideLegacyValue = overrideConfig?.[legacyKey];
63
+ if (typeof overrideLegacyValue === "boolean") {
64
+ return overrideLegacyValue;
65
+ }
66
+ const defaultMaskingValue = defaultConfig?.masking?.[maskingKey];
67
+ if (typeof defaultMaskingValue === "boolean") {
68
+ return defaultMaskingValue;
69
+ }
70
+ return true;
71
+ }
72
+ function resolveReplayConfig(defaultConfig, overrideConfig) {
73
+ const enabled = overrideConfig?.enabled ?? defaultConfig?.enabled ?? true;
74
+ const captureLog = overrideConfig?.captureLog ?? defaultConfig?.captureLog ?? true;
75
+ const captureNetworkTelemetry = overrideConfig?.captureNetworkTelemetry ?? defaultConfig?.captureNetworkTelemetry ?? true;
76
+ const requestedThrottleDelayMs = overrideConfig?.throttleDelayMs ?? defaultConfig?.throttleDelayMs ?? DEFAULT_THROTTLE_DELAY_MS;
77
+ const throttleDelayMs = Number.isFinite(requestedThrottleDelayMs) && requestedThrottleDelayMs >= 0 ? requestedThrottleDelayMs : DEFAULT_THROTTLE_DELAY_MS;
78
+ return {
79
+ enabled,
80
+ maskAllTextInputs: resolveMaskingValue(overrideConfig, defaultConfig, "textInputs", "maskAllTextInputs"),
81
+ maskAllImages: resolveMaskingValue(overrideConfig, defaultConfig, "images", "maskAllImages"),
82
+ maskAllSandboxedViews: resolveMaskingValue(overrideConfig, defaultConfig, "sandboxedViews", "maskAllSandboxedViews"),
83
+ captureLog,
84
+ captureNetworkTelemetry,
85
+ throttleDelayMs
86
+ };
87
+ }
88
+ function toNativeReplayConfig(config) {
89
+ return {
90
+ maskAllTextInputs: config.maskAllTextInputs,
91
+ maskAllImages: config.maskAllImages,
92
+ maskAllSandboxedViews: config.maskAllSandboxedViews,
93
+ captureLog: config.captureLog,
94
+ captureNetworkTelemetry: config.captureNetworkTelemetry,
95
+ throttleDelayMs: config.throttleDelayMs
96
+ };
97
+ }
98
+ function createMobileReplayRecorderAdapter(defaults) {
99
+ assertNonEmpty(defaults.apiKey, "apiKey");
100
+ assertNonEmpty(defaults.endpoint, "endpoint");
101
+ return {
102
+ start: async args => {
103
+ const options = args.options;
104
+ const resolvedReplayConfig = resolveReplayConfig(defaults.replayConfigDefaults, options?.replayConfig);
105
+ if (!resolvedReplayConfig.enabled) {
106
+ return () => {};
107
+ }
108
+ const host = normalizeHost(options?.replayHost?.trim() || defaults.endpoint);
109
+ assertNoPosthogDotCom(host);
110
+ const snapshotPath = normalizeSnapshotPath(options?.replaySnapshotPath);
111
+ const activeSessionId = resolveSessionId(options);
112
+ const sdkOptions = {
113
+ apiKey: defaults.apiKey,
114
+ host,
115
+ debug: false,
116
+ distinctId: options?.userId?.trim() || "",
117
+ anonymousId: "",
118
+ sdkVersion: "plushanalytics-react-native",
119
+ flushAt: 20
120
+ };
121
+ await replayModule.start(activeSessionId, sdkOptions, toNativeReplayConfig(resolvedReplayConfig), {
122
+ endpoint: snapshotPath
123
+ });
124
+ await replayModule.startSession(activeSessionId);
125
+ args.onSessionId?.(activeSessionId);
126
+ return () => {
127
+ void replayModule.endSession();
128
+ };
129
+ },
130
+ flush: async () => {
131
+ await replayModule.flush();
132
+ }
133
+ };
134
+ }
135
+ //# sourceMappingURL=mobileReplayAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["replayModule","_interopRequireWildcard","require","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","DEFAULT_SNAPSHOT_PATH","DEFAULT_THROTTLE_DELAY_MS","UUID_PATTERN","assertNonEmpty","value","field","trim","Error","normalizeHost","replace","normalizeSnapshotPath","trimmed","startsWith","isUuid","test","generateUuid","cryptoApi","globalThis","crypto","randomUUID","char","random","Math","floor","resolved","toString","resolveSessionId","options","requestedSessionId","sessionId","assertNoPosthogDotCom","host","lowered","toLowerCase","hostName","URL","hostname","endsWith","resolveMaskingValue","overrideConfig","defaultConfig","maskingKey","legacyKey","overrideMaskingValue","masking","overrideLegacyValue","defaultMaskingValue","resolveReplayConfig","enabled","captureLog","captureNetworkTelemetry","requestedThrottleDelayMs","throttleDelayMs","Number","isFinite","maskAllTextInputs","maskAllImages","maskAllSandboxedViews","toNativeReplayConfig","config","createMobileReplayRecorderAdapter","defaults","apiKey","endpoint","start","args","resolvedReplayConfig","replayConfigDefaults","replayConfig","replayHost","snapshotPath","replaySnapshotPath","activeSessionId","sdkOptions","debug","distinctId","userId","anonymousId","sdkVersion","flushAt","startSession","onSessionId","endSession","flush"],"sourceRoot":"../../../src","sources":["adapters/mobileReplayAdapter.ts"],"mappings":"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","ignoreList":[]}