Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,5 @@ lib/
# React Native Codegen
ios/generated
android/generated

.cursor/
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## Next

## 1.1.1 - 2025-06-24

- fix: check if distinctId and anonId are a string before using it
- chore: pin the iOS SDK to 3.21.x
- chore: pin the Android SDK to 3.19.1

## 1.1.0 - 2025-06-06

- chore: remove maskPhotoLibraryImages from the SDK config
Expand Down
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.PHONY: formatKotlin formatSwift

# brew install ktlint
formatKotlin:
ktlint -F "**/*.kt"

# brew install swift-format
formatSwift:
swiftformat ios --swiftversion 5.3
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,6 @@ dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.posthog:posthog-android:3.11.3"
implementation "com.posthog:posthog-android:3.19.1"
}

Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.posthogreactnativesessionreplay

import android.util.Log
import com.facebook.react.bridge.Promise
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just formatting

import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil

import com.posthog.PostHog
import com.posthog.PostHogConfig
import com.posthog.android.PostHogAndroid
Expand All @@ -18,88 +17,90 @@ import com.posthog.internal.PostHogPreferences.Companion.DISTINCT_ID
import com.posthog.internal.PostHogSessionManager
import java.util.UUID

class PosthogReactNativeSessionReplayModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {

override fun getName(): String {
return NAME
}
class PosthogReactNativeSessionReplayModule(
reactContext: ReactApplicationContext,
) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = NAME

@ReactMethod
fun start(
sessionId: String,
sdkOptions: ReadableMap,
sdkReplayConfig: ReadableMap,
decideReplayConfig: ReadableMap,
promise: Promise
promise: Promise,
) {
val initRunnable = Runnable {
try {
val uuid = UUID.fromString(sessionId)
PostHogSessionManager.setSessionId(uuid)

val context = this.reactApplicationContext
val apiKey = sdkOptions.getString("apiKey") ?: ""
val host = sdkOptions.getString("host") ?: PostHogConfig.DEFAULT_HOST
val debugValue = sdkOptions.getBoolean("debug")

val maskAllTextInputs = sdkReplayConfig.getBoolean("maskAllTextInputs")
val maskAllImages = sdkReplayConfig.getBoolean("maskAllImages")
val captureLog = sdkReplayConfig.getBoolean("captureLog")
val debouncerDelayMs = sdkReplayConfig.getInt("androidDebouncerDelayMs")

val endpoint = decideReplayConfig.getString("endpoint")

val distinctId = try {
sdkOptions.getString("distinctId") ?: ""
} catch (e: Throwable) {
logError("parse distinctId", e)
""
}
val anonymousId = try {
sdkOptions.getString("anonymousId") ?: ""
} catch (e: Throwable) {
logError("parse anonymousId", e)
""
}
val theSdkVersion = sdkOptions.getString("sdkVersion")

var theFlushAt = 20
if (sdkOptions.hasKey("flushAt")) {
theFlushAt = sdkOptions.getInt("flushAt")
}

val config = PostHogAndroidConfig(apiKey, host).apply {
debug = debugValue
captureDeepLinks = false
captureApplicationLifecycleEvents = false
captureScreenViews = false
flushAt = theFlushAt
sessionReplay = true
sessionReplayConfig.screenshot = true
sessionReplayConfig.captureLogcat = captureLog
sessionReplayConfig.debouncerDelayMs = debouncerDelayMs.toLong()
sessionReplayConfig.maskAllImages = maskAllImages
sessionReplayConfig.maskAllTextInputs = maskAllTextInputs

if (!endpoint.isNullOrEmpty()) {
snapshotEndpoint = endpoint
val initRunnable =
Runnable {
try {
val uuid = UUID.fromString(sessionId)
PostHogSessionManager.setSessionId(uuid)

val context = this.reactApplicationContext
val apiKey = sdkOptions.getString("apiKey") ?: ""
val host = sdkOptions.getString("host") ?: PostHogConfig.DEFAULT_HOST
val debugValue = sdkOptions.getBoolean("debug")

val maskAllTextInputs = sdkReplayConfig.getBoolean("maskAllTextInputs")
val maskAllImages = sdkReplayConfig.getBoolean("maskAllImages")
val captureLog = sdkReplayConfig.getBoolean("captureLog")
val debouncerDelayMs = sdkReplayConfig.getInt("androidDebouncerDelayMs")

val endpoint = decideReplayConfig.getString("endpoint")

val distinctId =
try {
sdkOptions.getString("distinctId") ?: ""
} catch (e: Throwable) {
logError("parse distinctId", e)
""
}
val anonymousId =
try {
sdkOptions.getString("anonymousId") ?: ""
} catch (e: Throwable) {
logError("parse anonymousId", e)
""
}
val theSdkVersion = sdkOptions.getString("sdkVersion")

var theFlushAt = 20
if (sdkOptions.hasKey("flushAt")) {
theFlushAt = sdkOptions.getInt("flushAt")
}

if (!theSdkVersion.isNullOrEmpty()) {
sdkName = "posthog-react-native"
sdkVersion = theSdkVersion
}
val config =
PostHogAndroidConfig(apiKey, host).apply {
debug = debugValue
captureDeepLinks = false
captureApplicationLifecycleEvents = false
captureScreenViews = false
flushAt = theFlushAt
sessionReplay = true
sessionReplayConfig.screenshot = true
sessionReplayConfig.captureLogcat = captureLog
sessionReplayConfig.throttleDelayMs = debouncerDelayMs.toLong()
sessionReplayConfig.maskAllImages = maskAllImages
sessionReplayConfig.maskAllTextInputs = maskAllTextInputs

if (!endpoint.isNullOrEmpty()) {
snapshotEndpoint = endpoint
}

if (!theSdkVersion.isNullOrEmpty()) {
sdkName = "posthog-react-native"
sdkVersion = theSdkVersion
}
}
PostHogAndroid.setup(context, config)

setIdentify(config.cachePreferences, distinctId, anonymousId)
} catch (e: Throwable) {
logError("start", e)
} finally {
promise.resolve(null)
}
PostHogAndroid.setup(context, config)

setIdentify(config.cachePreferences, distinctId, anonymousId)
} catch (e: Throwable) {
logError("start", e)
} finally {
promise.resolve(null)
}
}

// forces the SDK to be initialized on the main thread
if (UiThreadUtil.isOnUiThread()) {
Expand All @@ -110,7 +111,10 @@ class PosthogReactNativeSessionReplayModule(reactContext: ReactApplicationContex
}

@ReactMethod
fun startSession(sessionId: String, promise: Promise) {
fun startSession(
sessionId: String,
promise: Promise,
) {
try {
val uuid = UUID.fromString(sessionId)
PostHogSessionManager.setSessionId(uuid)
Expand Down Expand Up @@ -144,7 +148,11 @@ class PosthogReactNativeSessionReplayModule(reactContext: ReactApplicationContex
}

@ReactMethod
fun identify(distinctId: String, anonymousId: String, promise: Promise) {
fun identify(
distinctId: String,
anonymousId: String,
promise: Promise,
) {
try {
setIdentify(PostHog.getConfig<PostHogConfig>()?.cachePreferences, distinctId, anonymousId)
} catch (e: Throwable) {
Expand All @@ -157,7 +165,7 @@ class PosthogReactNativeSessionReplayModule(reactContext: ReactApplicationContex
private fun setIdentify(
cachePreferences: PostHogPreferences?,
distinctId: String,
anonymousId: String
anonymousId: String,
) {
cachePreferences?.let { preferences ->
if (anonymousId.isNotEmpty()) {
Expand All @@ -169,7 +177,10 @@ class PosthogReactNativeSessionReplayModule(reactContext: ReactApplicationContex
}
}

private fun logError(method: String, error: Throwable) {
private fun logError(
method: String,
error: Throwable,
) {
Log.println(Log.ERROR, POSTHOG_TAG, "Method $method, error: $error")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager


class PosthogReactNativeSessionReplayPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(PosthogReactNativeSessionReplayModule(reactContext))
}
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
listOf(PosthogReactNativeSessionReplayModule(reactContext))

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnable
import com.facebook.react.defaults.DefaultReactActivityDelegate

class MainActivity : ReactActivity() {

/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
Expand All @@ -17,6 +16,5 @@ class MainActivity : ReactActivity() {
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,24 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader

class MainApplication : Application(), ReactApplication {

class MainApplication :
Application(),
ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}

override fun getJSMainModuleName(): String = "index"
override fun getJSMainModuleName(): String = "index"

override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}

override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
Expand Down
Loading
Loading