Skip to content

Commit

Permalink
add basic feature flags api
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto committed Sep 14, 2023
1 parent 30ebddd commit 2a2d767
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
text = AnnotatedString("Hello $name!"),
modifier = modifier,
onClick = {
PostHog.capture("testEvent", mapOf("testProperty" to "testValue"))
// PostHog.capture("testEvent", mapOf("testProperty" to "testValue"))
// PostHog.reloadFeatureFlagsRequest()
// sessionRecording
PostHog.isFeatureEnabled("sessionRecording")
},
)
}
Expand Down
19 changes: 6 additions & 13 deletions posthog-v3/posthog/api/posthog.api
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ public final class com/posthog/PostHog {
public final fun close ()V
public final fun getAnonymousId ()Ljava/lang/String;
public final fun getDistinctId ()Ljava/lang/String;
public final fun isFeatureEnabled (Ljava/lang/String;Z)Z
public static synthetic fun isFeatureEnabled$default (Lcom/posthog/PostHog;Ljava/lang/String;ZILjava/lang/Object;)Z
public final fun reloadFeatureFlagsRequest ()V
public final fun screen (Ljava/lang/String;Ljava/util/Map;)V
public static synthetic fun screen$default (Lcom/posthog/PostHog;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V
public final fun setup (Lcom/posthog/PostHogConfig;)V
Expand All @@ -17,24 +20,16 @@ public final class com/posthog/PostHog$Companion {
public final fun capture (Ljava/lang/String;Ljava/util/Map;)V
public static synthetic fun capture$default (Lcom/posthog/PostHog$Companion;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)V
public final fun close ()V
public final fun isFeatureEnabled (Ljava/lang/String;Z)Z
public static synthetic fun isFeatureEnabled$default (Lcom/posthog/PostHog$Companion;Ljava/lang/String;ZILjava/lang/Object;)Z
public final fun reloadFeatureFlagsRequest ()V
public final fun setup (Lcom/posthog/PostHogConfig;)V
public final fun with (Lcom/posthog/PostHogConfig;)Lcom/posthog/PostHog;
}

public final class com/posthog/PostHogConfig {
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZIIIILcom/posthog/PostHogDataMode;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZIIIILcom/posthog/PostHogDataMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Z
public final fun component4 ()I
public final fun component5 ()I
public final fun component6 ()I
public final fun component7 ()I
public final fun component8 ()Lcom/posthog/PostHogDataMode;
public final fun copy (Ljava/lang/String;Ljava/lang/String;ZIIIILcom/posthog/PostHogDataMode;)Lcom/posthog/PostHogConfig;
public static synthetic fun copy$default (Lcom/posthog/PostHogConfig;Ljava/lang/String;Ljava/lang/String;ZIIIILcom/posthog/PostHogDataMode;ILjava/lang/Object;)Lcom/posthog/PostHogConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getApiKey ()Ljava/lang/String;
public final fun getDataMode ()Lcom/posthog/PostHogDataMode;
public final fun getDebug ()Z
Expand All @@ -43,14 +38,12 @@ public final class com/posthog/PostHogConfig {
public final fun getHost ()Ljava/lang/String;
public final fun getMaxBatchSize ()I
public final fun getMaxQueueSize ()I
public fun hashCode ()I
public final fun setDataMode (Lcom/posthog/PostHogDataMode;)V
public final fun setDebug (Z)V
public final fun setFlushAt (I)V
public final fun setFlushIntervalSeconds (I)V
public final fun setMaxBatchSize (I)V
public final fun setMaxQueueSize (I)V
public fun toString ()Ljava/lang/String;
}

public final class com/posthog/PostHogDataMode : java/lang/Enum {
Expand Down
32 changes: 32 additions & 0 deletions posthog-v3/posthog/src/main/java/com/posthog/PostHog.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.posthog

import com.posthog.internal.PostHogApi
import com.posthog.internal.PostHogFeatureFlags
import com.posthog.internal.PostHogPrintLogger
import com.posthog.internal.PostHogQueue
import com.posthog.internal.PostHogSessionManager
Expand All @@ -15,6 +16,7 @@ public class PostHog {
private var config: PostHogConfig? = null
private var storage: PostHogStorage? = null
private var sessionManager: PostHogSessionManager? = null
private var featureFlags: PostHogFeatureFlags? = null
private var api: PostHogApi? = null
private var queue: PostHogQueue? = null

Expand All @@ -36,14 +38,17 @@ public class PostHog {
sessionManager = PostHogSessionManager(storage)
val api = PostHogApi(config)
val queue = PostHogQueue(config, storage, api)
val featureFlags = PostHogFeatureFlags(config, api)

this.api = api
this.storage = storage
this.config = config
this.queue = queue
this.featureFlags = featureFlags
enabled = true

queue.start()
loadFeatureFlagsRequest()
}
}

Expand Down Expand Up @@ -125,6 +130,25 @@ public class PostHog {
capture("\$create_alias", properties = props)
}

public fun reloadFeatureFlagsRequest() {
if (!isEnabled()) {
return
}
loadFeatureFlagsRequest()
}

private fun loadFeatureFlagsRequest() {
val map = mapOf<String, Any>()
featureFlags?.loadFeatureFlagsRequest(buildProperties(map))
}

public fun isFeatureEnabled(key: String, defaultValue: Boolean = false): Boolean {
if (!isEnabled()) {
return defaultValue
}
return featureFlags?.isFeatureEnabled(key, defaultValue) ?: defaultValue
}

// TODO: groups, groupIdentify, group, feature flags, buildProperties (static context, dynamic context, distinct_id)

private fun isEnabled(): Boolean {
Expand Down Expand Up @@ -156,6 +180,14 @@ public class PostHog {
shared.capture(event, properties = properties)
}

public fun reloadFeatureFlagsRequest() {
shared.reloadFeatureFlagsRequest()
}

public fun isFeatureEnabled(key: String, defaultValue: Boolean = false): Boolean {
return shared.isFeatureEnabled(key, defaultValue = defaultValue)
}

// TODO: add other methods
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.posthog

import com.posthog.internal.PostHogLogger

public data class PostHogConfig(
public class PostHogConfig(
// apiKey and host are immutable due to offline caching
public val apiKey: String,
public val host: String = "https://app.posthog.com",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ internal class PostHogApi(private val config: PostHogConfig) {
private val gson = GsonBuilder().apply {
registerTypeAdapter(Date::class.java, GsonDateTypeAdapter(config))
}.create()
private val gsonBodyType = object : TypeToken<PostHogBatchEvent>() {}.type
private val gsonBatchBodyType = object : TypeToken<PostHogBatchEvent>() {}.type
private val gsonDecideBodyType = object : TypeToken<Map<String, Any>>() {}.type

fun batch(events: List<PostHogEvent>) {
val batch = PostHogBatchEvent(config.apiKey, events)
val json = gson.toJson(batch, gsonBodyType)
val json = gson.toJson(batch, gsonBatchBodyType)
// """
// {
// "api_key": "_6SG-F7I1vCuZ-HdJL3VZQqjBlaSb1_20hDPwqMNnGI",
Expand All @@ -50,7 +51,7 @@ internal class PostHogApi(private val config: PostHogConfig) {
.build()

client.newCall(request).execute().use {
if (!it.isSuccessful) throw PostHogApiError(it.code, it.message)
if (!it.isSuccessful) throw PostHogApiError(it.code, it.message, body = it.body)
// """
// {
// "status": 1
Expand All @@ -59,5 +60,63 @@ internal class PostHogApi(private val config: PostHogConfig) {
}
}

// TODO: decide, get APIs
fun decide(properties: Map<String, Any>): Map<String, Any>? {
val map = mutableMapOf<String, Any>()
map.putAll(properties)
map["api_key"] = config.apiKey

val json = gson.toJson(map, gsonDecideBodyType)
// """
// {
// "distinct_id": "1fc77c1a-5f98-43b3-bb77-7a2dd15fd13a",
// "api_key": "_6SG-F7I1vCuZ-HdJL3VZQqjBlaSb1_20hDPwqMNnGI"
// }
// """.trimIndent()
val body = json.toRequestBody(mediaType)

val request = Request.Builder()
.url("${config.host}/decide/?v=3")
.header("User-Agent", config.userAgent)
.post(body)
.build()

client.newCall(request).execute().use {
if (!it.isSuccessful) throw PostHogApiError(it.code, it.message, body = it.body)

it.body?.let { body ->
return gson.fromJson(body.string(), gsonDecideBodyType)
}
return null
}
// """
// {
// "config": {
// "enable_collect_everything": true
// },
// "toolbarParams": {},
// "isAuthenticated": false,
// "supportedCompression": [
// "gzip",
// "gzip-js"
// ],
// "featureFlags": {
// "4535-funnel-bar-viz": true
// },
// "sessionRecording": false,
// "errorsWhileComputingFlags": false,
// "featureFlagPayloads": {},
// "capturePerformance": true,
// "autocapture_opt_out": false,
// "autocaptureExceptions": false,
// "siteApps": [
// {
// "id": 21039,
// "url": "/site_app/21039/EOsOSePYNyTzHkZ3f4mjrjUap8Hy8o2vUTAc6v1ZMFP/576ac89bc8aed72a21d9b19221c2c626/"
// }
// ]
// }
// """.trimIndent()
}

// TODO: get APIs
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.posthog.internal

import okhttp3.ResponseBody
import java.lang.RuntimeException

// TODO: rete limit will be part of response in the future
internal data class PostHogApiError(val statusCode: Int, override val message: String) : RuntimeException(message)
internal class PostHogApiError(
val statusCode: Int,
override val message: String,
body: ResponseBody? = null,
) : RuntimeException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.posthog.internal

import com.posthog.PostHogConfig
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean

internal class PostHogFeatureFlags(private val config: PostHogConfig, private val api: PostHogApi) {
private val executor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("PostHogDecideThread"))

private var isLoadingFeatureFlags = AtomicBoolean(false)

private val featureFlagsLock = Any()

private var featureFlags: Map<String, Any>? = null

@Volatile
private var isFeatureFlagsLoaded = false

fun loadFeatureFlagsRequest(properties: Map<String, Any>) {
executor.execute {
if (isLoadingFeatureFlags.getAndSet(true)) {
config.logger?.log("Feature flags are being loaded already.")
return@execute
}

try {
val featureFlags = api.decide(properties)

synchronized(featureFlagsLock) {
this.featureFlags = featureFlags
}

isFeatureFlagsLoaded = true
} catch (e: Throwable) {
config.logger?.log("Loading feature flags failed: $e")
}

isLoadingFeatureFlags.set(false)
}
}

fun isFeatureEnabled(key: String, defaultValue: Boolean = false): Boolean {
if (!isFeatureFlagsLoaded) {
return defaultValue
}
val value: Any?

synchronized(featureFlagsLock) {
value = featureFlags?.get(key)
}

return if (value is Boolean) {
value
} else {
defaultValue
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@ internal class PostHogQueue(private val config: PostHogConfig, private val stora
private var timerTask: TimerTask? = null

// https://github.com/square/tape/blob/master/tape/src/main/java/com/squareup/tape2/QueueFile.java
@Volatile
private var isFlushing = AtomicBoolean(false)

private val delay: Long get() = (config.flushIntervalSeconds * 1000).toLong()

private val executor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory())
private val executor = Executors.newSingleThreadScheduledExecutor(PostHogThreadFactory("PostHogBatchThread"))

// init {
// TOOD: load deque from disk
Expand Down Expand Up @@ -112,8 +111,7 @@ internal class PostHogQueue(private val config: PostHogConfig, private val stora

fun start() {
synchronized(timerLock) {
timerTask?.cancel()
timer?.cancel()
stopTimer()
val timer = Timer(true)
val timerTask = timer.schedule(delay, delay) {
// early check to avoid more checks when its already flushing
Expand All @@ -128,9 +126,14 @@ internal class PostHogQueue(private val config: PostHogConfig, private val stora
}
}

private fun stopTimer() {
timerTask?.cancel()
timer?.cancel()
}

fun stop() {
synchronized(timerLock) {
timer?.cancel()
stopTimer()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package com.posthog.internal

import java.util.concurrent.ThreadFactory

internal class PostHogThreadFactory : ThreadFactory {
internal class PostHogThreadFactory(private val threadName: String) : ThreadFactory {
override fun newThread(runnable: Runnable): Thread {
return Thread(runnable).apply {
isDaemon = true
name = "PostHogThreadFactory"
name = threadName
}
}
}

0 comments on commit 2a2d767

Please sign in to comment.