Skip to content

Commit

Permalink
migrate to okhttp
Browse files Browse the repository at this point in the history
  • Loading branch information
kenny-statsig committed Sep 13, 2023
1 parent a6f6ed6 commit d421b69
Show file tree
Hide file tree
Showing 19 changed files with 391 additions and 292 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,14 @@ dependencies {

implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'

testImplementation 'junit:junit:4.12'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0'
testImplementation "io.mockk:mockk:1.12.0"
testImplementation 'com.github.tomakehurst:wiremock:2.27.2'
testImplementation "org.slf4j:slf4j-simple:1.8.0-beta4"
testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0")
}

artifacts {
Expand Down
26 changes: 16 additions & 10 deletions src/main/java/com/statsig/androidsdk/ErrorBoundary.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package com.statsig.androidsdk

import com.google.gson.Gson
import kotlinx.coroutines.CoroutineExceptionHandler
import java.io.DataOutputStream
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.lang.RuntimeException
import java.net.HttpURLConnection
import java.net.URL
import kotlin.math.floor

Expand Down Expand Up @@ -107,15 +109,19 @@ internal class ErrorBoundary() {
)
val postData = Gson().toJson(body)

val conn = url.openConnection() as HttpURLConnection
conn.requestMethod = "POST"
conn.doOutput = true
conn.setRequestProperty("Content-Type", "application/json")
conn.setRequestProperty("STATSIG-API-KEY", apiKey)
conn.useCaches = false
val clientBuilder = OkHttpClient.Builder()

DataOutputStream(conn.outputStream).use { it.writeBytes(postData) }
conn.responseCode // triggers request
clientBuilder.addInterceptor(RequestHeaderInterceptor(apiKey!!))
clientBuilder.addInterceptor(ResponseInterceptor())

val httpClient = clientBuilder.build()

val requestBody: RequestBody = postData.toRequestBody(JSON)
val request: Request = Request.Builder()
.url(url)
.post(requestBody)
.build()
httpClient.newCall(request).execute()
} catch (e: Exception) {
// noop
}
Expand Down
73 changes: 73 additions & 0 deletions src/main/java/com/statsig/androidsdk/HttpUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.statsig.androidsdk

import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.net.HttpURLConnection


private val RETRY_CODES: IntArray = intArrayOf(
HttpURLConnection.HTTP_CLIENT_TIMEOUT,
HttpURLConnection.HTTP_INTERNAL_ERROR,
HttpURLConnection.HTTP_BAD_GATEWAY,
HttpURLConnection.HTTP_UNAVAILABLE,
HttpURLConnection.HTTP_GATEWAY_TIMEOUT,
522,
524,
599,
)
private const val CONTENT_TYPE_HEADER_KEY = "Content-Type"
private const val CONTENT_TYPE_HEADER_VALUE = "application/json; charset=UTF-8"
private const val STATSIG_API_HEADER_KEY = "STATSIG-API-KEY"
private const val STATSIG_CLIENT_TIME_HEADER_KEY = "STATSIG-CLIENT-TIME"
private const val STATSIG_SDK_TYPE_KEY = "STATSIG-SDK-TYPE"
private const val STATSIG_SDK_VERSION_KEY = "STATSIG-SDK-VERSION"
private const val ACCEPT_HEADER_KEY = "Accept"
private const val ACCEPT_HEADER_VALUE = "application/json"
internal val JSON: MediaType = "application/json; charset=utf-8".toMediaType();

class RequestHeaderInterceptor(private val sdkKey: String) : Interceptor {
@Throws(Exception::class)
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val request = original.newBuilder()
.addHeader(CONTENT_TYPE_HEADER_KEY, CONTENT_TYPE_HEADER_VALUE)
.addHeader(STATSIG_API_HEADER_KEY, sdkKey)
.addHeader(STATSIG_SDK_TYPE_KEY, "android-client")
.addHeader(STATSIG_SDK_VERSION_KEY, BuildConfig.VERSION_NAME)
.addHeader(STATSIG_CLIENT_TIME_HEADER_KEY, System.currentTimeMillis().toString())
.addHeader(ACCEPT_HEADER_KEY, ACCEPT_HEADER_VALUE)
.method(original.method, original.body)
.build()
return chain.proceed(request)
}
}

class ResponseInterceptor : Interceptor {
@Throws(Exception::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response = chain.proceed(request)

var attempt = 1
var retries = 0
if (LOGGING_ENDPOINT in request.url.pathSegments) {
retries = 3
}
while (!response.isSuccessful && attempt <= retries && response.code in RETRY_CODES) {
attempt++

response.close()
response = chain.proceed(request)
}

val bodyString = response.body?.string()

return response.newBuilder()
.body(bodyString?.toResponseBody(response.body?.contentType()))
.addHeader("attempt", attempt.toString())
.build()
}
}
6 changes: 4 additions & 2 deletions src/main/java/com/statsig/androidsdk/InitializeResponse.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.statsig.androidsdk

import com.google.gson.JsonObject
import com.google.gson.JsonSerializer
import com.google.gson.annotations.SerializedName
import java.lang.Exception


enum class InitializeFailReason {
CoroutineTimeout,
Expand Down Expand Up @@ -33,7 +35,7 @@ internal data class APIFeatureGate(
@SerializedName("secondary_exposures") val secondaryExposures: Array<Map<String, String>> = arrayOf(),
)

internal data class APIDynamicConfig(
internal data class APIDynamicConfig (
@SerializedName("name") val name: String,
@SerializedName("value") val value: Map<String, Any>,
@SerializedName("rule_id") val ruleID: String?,
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/statsig/androidsdk/Layer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package com.statsig.androidsdk
class Layer internal constructor(
private val client: StatsigClient?,
private val name: String,
private val jsonValue: Map<String, Any>,
public val jsonValue: Map<String, Any>,
private val rule: String,
private val details: EvaluationDetails,
private val secondaryExposures: Array<Map<String, String>> = arrayOf(),
Expand Down
16 changes: 10 additions & 6 deletions src/main/java/com/statsig/androidsdk/StatsigClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ internal class StatsigClient() {
private lateinit var user: StatsigUser
private lateinit var application: Application
private lateinit var sdkKey: String
private lateinit var options: StatsigOptions
private lateinit var lifecycleListener: StatsigActivityLifecycleListener
private lateinit var logger: StatsigLogger
private lateinit var statsigMetadata: StatsigMetadata
Expand All @@ -44,7 +43,9 @@ internal class StatsigClient() {
private val isBootstrapped = AtomicBoolean(false)

@VisibleForTesting
internal var statsigNetwork: StatsigNetwork = StatsigNetwork()
internal lateinit var statsigNetwork: StatsigNetwork
@VisibleForTesting
internal lateinit var options: StatsigOptions

fun initializeAsync(
application: Application,
Expand Down Expand Up @@ -98,7 +99,6 @@ internal class StatsigClient() {
}
val initResponse = statsigNetwork.initialize(
this@StatsigClient.options.api,
this@StatsigClient.sdkKey,
user,
this@StatsigClient.store.getLastUpdateTime(this@StatsigClient.user),
this@StatsigClient.statsigMetadata,
Expand All @@ -118,7 +118,7 @@ internal class StatsigClient() {

this@StatsigClient.pollForUpdates()

this@StatsigClient.statsigNetwork.apiRetryFailedLogs(this@StatsigClient.options.api, this@StatsigClient.sdkKey)
this@StatsigClient.statsigNetwork.apiRetryFailedLogs(this@StatsigClient.options.api)
this@StatsigClient.diagnostics.markEnd(KeyType.OVERALL, success)
logger.logDiagnostics()
InitializationDetails(duration, success, if (initResponse is InitializeResponse.FailedInitializeResponse) initResponse else null)
Expand Down Expand Up @@ -157,6 +157,11 @@ internal class StatsigClient() {
exceptionHandler = Statsig.errorBoundary.getExceptionHandler()
statsigScope = CoroutineScope(statsigJob + dispatcherProvider.main + exceptionHandler)

// Prevent overwriting mocked network in tests
if (!this::statsigNetwork.isInitialized) {
statsigNetwork = StatsigNetwork(sdkKey)
}

lifecycleListener = StatsigActivityLifecycleListener()
application.registerActivityLifecycleCallbacks(lifecycleListener)
logger = StatsigLogger(
Expand Down Expand Up @@ -408,7 +413,6 @@ internal class StatsigClient() {

val initResponse = statsigNetwork.initialize(
options.api,
sdkKey,
this@StatsigClient.user,
sinceTime,
statsigMetadata,
Expand Down Expand Up @@ -569,7 +573,7 @@ internal class StatsigClient() {
}
pollingJob?.cancel()
val sinceTime = store.getLastUpdateTime(user)
pollingJob = statsigNetwork.pollForChanges(options.api, sdkKey, user, sinceTime, statsigMetadata).onEach {
pollingJob = statsigNetwork.pollForChanges(options.api, user, sinceTime, statsigMetadata).onEach {
if (it?.hasUpdates == true) {
store.save(it, user)
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/statsig/androidsdk/StatsigLogger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ internal class StatsigLogger(
}
val flushEvents = ArrayList(events)
events = ConcurrentLinkedQueue()
statsigNetwork.apiPostLogs(api, sdkKey, gson.toJson(LogEventData(flushEvents, statsigMetadata)))
statsigNetwork.apiPostLogs(api, gson.toJson(LogEventData(flushEvents, statsigMetadata)))
}
}

Expand Down
Loading

0 comments on commit d421b69

Please sign in to comment.