Skip to content

Conversation

dustinbyrne
Copy link
Contributor

@dustinbyrne dustinbyrne commented Sep 22, 2025

This is a revival of #225:

  • Create PostHogStateless base class
  • Add PostHogStatelessInterface
  • Refactor PostHog to extend PostHogStateless
  • Update all method signatures to use stateless pattern
  • Add stateless tests

Additionally, the goal here is to avoid introducing any functional changes to the Android SDK or public interfaces.

Related to PostHog/posthog#16419

💡 Motivation and Context

To expand the core library to support stateless server-side Java applications. Additional Java interoperability changes should follow in another release.

💚 How did you test it?

According to the previous PR:

No new server-side code here, so existing Android tests should suffice for confidence in these changes.

I've added additional unit tests to validate stateless behavior.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

@dustinbyrne dustinbyrne force-pushed the feat/stateless-iface-2 branch 4 times, most recently from d0d4433 to f32533e Compare September 23, 2025 18:29
- Create PostHogStateless base class
- Add PostHogStatelessInterface
- Refactor PostHog to extend PostHogStateless
- Update all method signatures to use stateless pattern
- Fix compatibility issues
@dustinbyrne dustinbyrne force-pushed the feat/stateless-iface-2 branch from f32533e to 72e8663 Compare September 23, 2025 18:35
@dustinbyrne dustinbyrne marked this pull request as ready for review September 23, 2025 19:15
@dustinbyrne dustinbyrne requested a review from a team as a code owner September 23, 2025 19:15
@dustinbyrne dustinbyrne changed the title feat(java-sdk): Add a stateless interface feat(java-sdk): Add a server-side stateless interface Sep 23, 2025
@dustinbyrne dustinbyrne requested a review from a team September 23, 2025 20:25
Comment on lines 120 to 139
private var isPersonProcessingEnabled: Boolean = false
get() {
synchronized(personProcessingLock) {
if (!isPersonProcessingLoaded) {
isPersonProcessingEnabled = getPreferences().getValue(PERSON_PROCESSING) as? Boolean
?: false
isPersonProcessingLoaded = true
}
}
return field
}
set(value) {
synchronized(personProcessingLock) {
// only set if it's different to avoid IO since this is called more often
if (field != value) {
field = value
getPreferences().setValue(PERSON_PROCESSING, value)
}
}
}
Copy link
Contributor Author

@dustinbyrne dustinbyrne Sep 23, 2025

Choose a reason for hiding this comment

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

This is moved, unchanged, from PostHog.isPersonProcessingEnabled

Comment on lines +211 to +227
protected fun mergeGroups(givenGroups: Map<String, String>?): Map<String, String>? {
val preferences = getPreferences()

@Suppress("UNCHECKED_CAST")
val groups = preferences.getValue(GROUPS) as? Map<String, String>
val newGroups = mutableMapOf<String, String>()

groups?.let {
newGroups.putAll(it)
}

givenGroups?.let {
newGroups.putAll(it)
}

return newGroups.ifEmpty { null }
}
Copy link
Contributor Author

@dustinbyrne dustinbyrne Sep 23, 2025

Choose a reason for hiding this comment

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

This is moved, unchanged, from PostHog.mergeGroups

Comment on lines +285 to +312
@Suppress("DEPRECATION")
protected fun buildEvent(
event: String,
distinctId: String,
properties: MutableMap<String, Any>,
): PostHogEvent? {
// sanitize the properties or fallback to the original properties
val sanitizedProperties = config?.propertiesSanitizer?.sanitize(properties)?.toMutableMap() ?: properties
val postHogEvent = PostHogEvent(event, distinctId, properties = sanitizedProperties)
var eventChecked: PostHogEvent? = postHogEvent

val beforeSendList = config?.beforeSendList ?: emptyList()

for (beforeSend in beforeSendList) {
try {
eventChecked = beforeSend.run(postHogEvent)
if (eventChecked == null) {
config?.logger?.log("Event $event was rejected in beforeSend function")
return null
}
} catch (e: Throwable) {
config?.logger?.log("Error in beforeSend function: $e")
return null
}
}

return eventChecked
}
Copy link
Contributor Author

@dustinbyrne dustinbyrne Sep 23, 2025

Choose a reason for hiding this comment

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

This is moved, unchanged, from PostHog.buildEvent

Comment on lines 403 to 415
protected fun requirePersonProcessing(
functionName: String,
ignoreMessage: Boolean = false,
): Boolean {
if (config?.personProfiles == PersonProfiles.NEVER) {
if (!ignoreMessage) {
config?.logger?.log("$functionName was called, but `personProfiles` is set to `never`. This call will be ignored.")
}
return false
}
isPersonProcessingEnabled = true
return true
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is moved, unchanged, from PostHog.requirePersonProcessing

Comment on lines 393 to 401
private fun hasPersonProcessing(): Boolean {
return !(
config?.personProfiles == PersonProfiles.NEVER ||
(
config?.personProfiles == PersonProfiles.IDENTIFIED_ONLY &&
!isPersonProcessingEnabled
)
)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is moved with a slight logical change (isIdentified is no longer checked) from PostHog.hasPersonProcessing

Copy link
Member

Choose a reason for hiding this comment

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

so the caller should check isIdentified then otherwise it would be different than before

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. isIdentified is being called again (see #284 (comment)).

Comment on lines 391 to 392

props["\$process_person_profile"] = hasPersonProcessing()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was also removed by the previous branch. Is this correct?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I moved all the logic for person processing flags and person profiles back into their original locations (72cdf86). Looking at other server-side/stateless SDKs, it doesn't look like this needs to be a consideration.

}
}

private fun buildProperties(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This implementation does differ from the method in PostHog.kt. To my eyes it looked to remove behavior related to session replays, which seems correct.

Comment on lines 546 to 549
override fun isFeatureEnabled(
key: String,
defaultValue: Boolean,
): Boolean {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Re-adding this may have been a mistake during rebase: c5fc361

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this was not intended to be added back in. I've fixed this in d4485b2.

public fun alias (Ljava/lang/String;)V
public fun capture (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V
public fun close ()V
public fun debug (Z)V
Copy link
Member

Choose a reason for hiding this comment

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

why did we have the debug one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It moved into PostHogStateless. It should still be callable from this class given we now inherit from PostHogStateless.

@marandaneto
Copy link
Member

I've not tested anything, one thing to consider is that these changes should not affect react native and flutter
so we have to compile both SDKs pointing to this branch and make sure compilation/feature behaviour stays the same since both SDKs rely on this SDK to do native things

interface

Looking at other server-side SDKs, these don't look to be considered.
@dustinbyrne
Copy link
Contributor Author

dustinbyrne commented Sep 24, 2025

@marandaneto Thanks for the insights, they've been super helpful. I was able to compile against this branch in both posthog-flutter and posthog-react-native-session-replay. Running the examples, both projects appear to be working as expected.

@marandaneto
Copy link
Member

@dustinbyrne https://github.com/PostHog/posthog-android/blob/main/RELEASING.md
let me know if you need any assistance

@dustinbyrne dustinbyrne merged commit c3ca27d into main Sep 25, 2025
9 checks passed
@dustinbyrne dustinbyrne deleted the feat/stateless-iface-2 branch September 25, 2025 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants