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
5 changes: 5 additions & 0 deletions .changeset/wrapper-build-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog': patch
---

Add internal APIs so wrapper SDKs can run their own `beforeSend` on the fully built event before it's sent
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ apiValidation {
ignoredProjects.add("posthog-android-sample")
// Pre-1.0 (0.x) module β€” public surface may change between minor versions until 1.0.0.
ignoredProjects.add("posthog-android-surveys-compose")
// APIs only wrapper SDKs and the SDK's own modules may call; not part of the public API.
nonPublicMarkers.add("com.posthog.PostHogInternal")
}

nexusPublishing.postHogConfig()
14 changes: 0 additions & 14 deletions posthog-android/api/posthog-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,6 @@ public final class com/posthog/android/errortracking/PostHogNativeCrashIntegrati
public fun uninstall ()V
}

public final class com/posthog/android/internal/MainHandler {
public fun <init> ()V
public fun <init> (Landroid/os/Looper;)V
public synthetic fun <init> (Landroid/os/Looper;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getHandler ()Landroid/os/Handler;
public final fun getMainLooper ()Landroid/os/Looper;
}

public final class com/posthog/android/internal/PostHogAndroidUtilsKt {
public static final fun base64 (Landroid/graphics/Bitmap;Landroid/graphics/Bitmap$CompressFormat;I)Ljava/lang/String;
public static synthetic fun base64$default (Landroid/graphics/Bitmap;Landroid/graphics/Bitmap$CompressFormat;IILjava/lang/Object;)Ljava/lang/String;
public static final fun getApplicationInfo (Landroid/content/Context;)Landroid/content/pm/ApplicationInfo;
}

public abstract interface class com/posthog/android/replay/PostHogDrawableConverter {
public abstract fun convert (Landroid/graphics/drawable/Drawable;)Landroid/graphics/Bitmap;
}
Expand Down
900 changes: 0 additions & 900 deletions posthog/api/posthog.api

Large diffs are not rendered by default.

564 changes: 457 additions & 107 deletions posthog/src/main/java/com/posthog/PostHog.kt

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions posthog/src/main/java/com/posthog/PostHogInterface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -577,4 +577,68 @@ public interface PostHogInterface : PostHogCoreInterface {

@PostHogInternal
public fun <T : PostHogConfig> getConfig(): T?

/**
* For wrapper SDKs only (Flutter, React Native); apps use [capture] and [captureAi].
*
* Builds the event that [capture] (or [captureAi] when [ai] is true) would send: the same
* checks, SDK and registered properties, per-call values, session, screen and options, with a
* new uuid and timestamp. It doesn't run `beforeSend` and doesn't queue the event, so a wrapper
* can run its own hook on the full event, then pass the result to [sendBuiltEventForWrapper].
* It changes no SDK state, so a wrapper can drop the event: the current screen, the person
* properties used for flags, person processing turned on by [userProperties],
* [userPropertiesSetOnce] or [groups], and the replay debug window are updated by
* [sendBuiltEventForWrapper]. That send uses the event name passed here to decide whether the
* replay debug bundle applies, even if the wrapper's hook renamed the event.
*
* Returns null when the SDK wouldn't capture the event: disabled, opted out, an ignored
* exception type, or a `$screen` event (not on the AI lane) without a non-blank `$screen_name`. Defaults to null.
*
* ```kotlin
* val event = PostHog.buildEventForWrapper("purchase", properties = mapOf("plan" to "pro"))
* ?: return
* PostHog.sendBuiltEventForWrapper(wrapperBeforeSend(event) ?: return)
* ```
*
* @suppress
*/
@PostHogInternal
public fun buildEventForWrapper(
event: String,
distinctId: String? = null,
properties: Map<String, Any>? = null,
userProperties: Map<String, Any>? = null,
userPropertiesSetOnce: Map<String, Any>? = null,
groups: Map<String, String>? = null,
options: Map<String, Any?>? = null,
ai: Boolean = false,
timestamp: Date? = null,
): PostHogEvent? = null

/**
* For wrapper SDKs only (Flutter, React Native); apps use [capture] and [captureAi].
*
* Sends an event from [buildEventForWrapper] without adding properties again: runs the native
* `beforeSend` chain, then queues it on the analytics lane, or the AI lane when [ai] is true.
* The event keeps its uuid and timestamp. Legacy option properties are moved into options when
* the event is sent, as for [capture].
* Surveys and session replay event triggers see [event] as passed in, after the wrapper's hook.
* Like [capture], it updates the person properties used for flags from `$set` and `$set_once`,
* caches a `$screen` event's `$screen_name` as the current screen, turns person processing on
* when the build had user properties or groups, and may add the replay debug values snapshotted
* at build that the event lacks. Keys the wrapper's hook removed aren't added back.
*
* Person processing and the person properties for flags are skipped when `reset()` or
* `identify()` changed the user since the build. The event is dropped when this instance didn't
* build it, when `setup()` or `close()` ran since the build, or when it was already sent.
*
* Returns the event's uuid once it's queued, or null when it's dropped. Defaults to null.
*
* @suppress
*/
@PostHogInternal
public fun sendBuiltEventForWrapper(
event: PostHogEvent,
ai: Boolean = false,
): UUID? = null
}
41 changes: 31 additions & 10 deletions posthog/src/main/java/com/posthog/PostHogStateless.kt
Original file line number Diff line number Diff line change
Expand Up @@ -308,16 +308,37 @@ public open class PostHogStateless protected constructor(
timestamp: Date? = null,
options: MutableMap<String, Any?>? = null,
uuid: UUID? = null,
): PostHogEvent? = runBeforeSend(newEvent(event, distinctId, properties, timestamp, options, uuid), uuid)

/**
* Creates the event, without running `beforeSend`. A null [timestamp] means now, and a null
* [uuid] gets a new UUID v7.
*/
internal fun newEvent(
event: String,
distinctId: String,
properties: MutableMap<String, Any>,
timestamp: Date? = null,
options: MutableMap<String, Any?>? = null,
uuid: UUID? = null,
): PostHogEvent =
PostHogEvent(
event,
distinctId,
properties = properties,
timestamp = timestamp ?: config?.dateProvider?.currentDate() ?: Date(),
uuid = uuid ?: TimeBasedEpochGenerator.generate(),
options = options,
)

/**
* Runs the `beforeSend` chain on [postHogEvent]. Returns null when a hook drops the event or
* throws. When [uuid] is given, the result keeps it.
*/
internal fun runBeforeSend(
postHogEvent: PostHogEvent,
uuid: UUID?,
): PostHogEvent? {
val postHogEvent =
PostHogEvent(
event,
distinctId,
properties = properties,
timestamp = timestamp ?: config?.dateProvider?.currentDate() ?: Date(),
uuid = uuid ?: TimeBasedEpochGenerator.generate(),
options = options,
)
var eventChecked: PostHogEvent = postHogEvent

val beforeSendList = config?.beforeSendList ?: emptyList()
Expand All @@ -326,7 +347,7 @@ public open class PostHogStateless protected constructor(
try {
val result = beforeSend.run(eventChecked)
if (result == null) {
config?.logger?.log("Event $event was rejected in beforeSend function")
config?.logger?.log("Event ${postHogEvent.event} was rejected in beforeSend function")
return null
}
eventChecked = result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,14 @@ public class PostHogSessionReplayHandlerFake(private var isActive: Boolean) : Po
return isActive
}

// runs after each debugProperties() read, so a test can change the state between reads
public var onDebugProperties: (() -> Unit)? = null

override fun debugProperties(): Map<String, Any> {
if (throwOnDebugProperties) {
throw RuntimeException("debugProperties failed")
}
return debugProperties
return debugProperties.also { onDebugProperties?.invoke() }
}

override fun onEvent(
Expand Down
Loading
Loading