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
1 change: 1 addition & 0 deletions app/auth/auth-core-public/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ hedvig {
dependencies {
api(libs.kotlinx.datetime)
api(projects.authCoreApi)
api(libs.ktor.client.core)
api(projects.authlib)

implementation(libs.androidx.datastore.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,29 @@ package com.hedvig.android.auth.di

import com.hedvig.android.authlib.AuthEnvironment
import com.hedvig.android.authlib.AuthRepository
import com.hedvig.android.authlib.NetworkAuthRepository
import com.hedvig.android.authlib.networkAuthRepositoryWithEngine
import com.hedvig.android.core.buildconstants.HedvigBuildConstants
import com.hedvig.android.core.common.di.AppScope
import com.hedvig.android.core.common.di.AuthHttpClientEngine
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import io.ktor.client.engine.HttpClientEngine

@ContributesTo(AppScope::class)
interface AuthMetroProviders {
@Provides
@SingleIn(AppScope::class)
fun provideAuthRepository(hedvigBuildConstants: HedvigBuildConstants): AuthRepository = NetworkAuthRepository(
fun provideAuthRepository(
hedvigBuildConstants: HedvigBuildConstants,
@AuthHttpClientEngine engine: HttpClientEngine,
): AuthRepository = networkAuthRepositoryWithEngine(
environment = if (hedvigBuildConstants.isProduction) {
AuthEnvironment.PRODUCTION
} else {
AuthEnvironment.STAGING
},
additionalHttpHeadersProvider = { emptyMap() },
engine = engine,
)
}
7 changes: 7 additions & 0 deletions app/authlib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ kotlin {
implementation(libs.ktor.client.logging)
}
jvmMain.dependencies {
api(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
}
jvmTest.dependencies {
implementation(kotlin("test"))
implementation(libs.assertK)
implementation(libs.coroutines.test)
implementation(libs.ktor.client.mock)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ import kotlinx.io.IOException

private const val POLL_DELAY_MILLIS = 1000L

public class NetworkAuthRepository(
public class NetworkAuthRepository internal constructor(
environment: AuthEnvironment,
additionalHttpHeadersProvider: () -> Map<String, String>,
engine: HttpClientEngine?,
) : AuthRepository {
private val ktorClient: HttpClient = buildKtorClient(additionalHttpHeadersProvider)
public constructor(
environment: AuthEnvironment,
additionalHttpHeadersProvider: () -> Map<String, String>,
) : this(environment, additionalHttpHeadersProvider, null)

private val ktorClient: HttpClient = buildKtorClient(additionalHttpHeadersProvider, engine)

private val authService = AuthService(environment, ktorClient)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.hedvig.android.authlib.internal

import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
Expand All @@ -13,12 +14,21 @@ import io.ktor.client.request.header
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json

internal fun buildKtorClient(additionalHttpHeadersProvider: () -> Map<String, String>): HttpClient {
internal fun buildKtorClient(
additionalHttpHeadersProvider: () -> Map<String, String>,
engine: HttpClientEngine?,
): HttpClient {
val httpClientConfig: HttpClientConfig<*>.() -> Unit = {
commonKtorConfiguration(additionalHttpHeadersProvider).invoke(this)
}
return HttpClient(httpClientEngineFactory()) {
httpClientConfig()
return if (engine != null) {
HttpClient(engine) {
httpClientConfig()
}
} else {
HttpClient(httpClientEngineFactory()) {
httpClientConfig()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.hedvig.android.authlib

import io.ktor.client.engine.HttpClientEngine

/**
* Builds an [AuthRepository] over a caller-supplied [engine], so the host application can attach its
* own network observability to the auth client.
*
* This lives in the JVM source set on purpose. [HttpClientEngine] stays out of the shared surface
* exported to Obj-C, so the framework keeps exactly one initializer for [NetworkAuthRepository].
*/
public fun networkAuthRepositoryWithEngine(
environment: AuthEnvironment,
additionalHttpHeadersProvider: () -> Map<String, String>,
engine: HttpClientEngine,
): AuthRepository = NetworkAuthRepository(environment, additionalHttpHeadersProvider, engine)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.hedvig.android.authlib

import assertk.assertThat
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.respond
import io.ktor.http.HttpStatusCode
import kotlin.test.Test
import kotlinx.coroutines.test.runTest

class NetworkAuthRepositoryEngineTest {
/**
* The host application supplies an engine so it can attach its own network observability. Nothing
* else observes that the engine is honoured, so without this the wiring can be dropped while every
* other test, and the app itself, keeps passing.
*/
@Test
fun `requests are sent through the engine the caller supplied`() = runTest {
val engine = MockEngine { respond(content = "", status = HttpStatusCode.InternalServerError) }

val repository = networkAuthRepositoryWithEngine(
environment = AuthEnvironment.STAGING,
additionalHttpHeadersProvider = { emptyMap() },
engine = engine,
)
repository.startLoginAttempt(LoginMethod.SE_BANKID, OtpMarket.SE)

assertThat(engine.requestHistory).hasSize(1)
}

@Test
fun `the environment decides which host the request reaches`() = runTest {
val engine = MockEngine { respond(content = "", status = HttpStatusCode.InternalServerError) }

val repository = networkAuthRepositoryWithEngine(
environment = AuthEnvironment.PRODUCTION,
additionalHttpHeadersProvider = { emptyMap() },
engine = engine,
)
repository.startLoginAttempt(LoginMethod.SE_BANKID, OtpMarket.SE)

assertThat(engine.requestHistory.single().url.host).isEqualTo("auth.prod.hedvigit.com")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ annotation class IoDispatcher
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class BaseHttpClient

/** Ktor engine for the auth client, carrying the app's network observability hooks. */
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class AuthHttpClientEngine
3 changes: 3 additions & 0 deletions app/datadog/datadog-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ dependencies {

implementation(libs.datadog.sdk.core)
implementation(libs.datadog.sdk.logs)
implementation(libs.datadog.sdk.okhttp)
implementation(libs.datadog.sdk.rum)
implementation(libs.datadog.sdk.trace.otel)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.timber)
implementation(projects.authCorePublic)
implementation(projects.coreBuildConstants)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.datadog.android.rum.model.ErrorEvent.Category.EXCEPTION
import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy
import com.datadog.android.trace.opentelemetry.DatadogOpenTelemetry
import com.hedvig.android.core.buildconstants.HedvigBuildConstants
import com.hedvig.android.datadog.core.di.authHost
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import io.opentelemetry.api.GlobalOpenTelemetry
Expand All @@ -40,7 +41,12 @@ abstract class DatadogInitializer : Initializer<Unit> {
service = "android",
)
.useSite(DatadogSite.EU1)
.setFirstPartyHosts(listOf(hedvigBuildConstants.urlGraphqlOctopus.removePrefix("https://")))
.setFirstPartyHosts(
listOf(
hedvigBuildConstants.urlGraphqlOctopus.removePrefix("https://"),
authHost(hedvigBuildConstants),
),
)
.build()
val sdkCore = Datadog.initialize(context, configuration, TrackingConsent.GRANTED)
if (sdkCore == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.hedvig.android.datadog.core.di

import com.datadog.android.core.sampling.RateBasedSampler
import com.datadog.android.okhttp.DatadogEventListener
import com.datadog.android.okhttp.DatadogInterceptor
import com.datadog.android.okhttp.trace.TracingInterceptor
import com.hedvig.android.core.buildconstants.HedvigBuildConstants
import com.hedvig.android.core.common.di.AppScope
import com.hedvig.android.core.common.di.AuthHttpClientEngine
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.okhttp.OkHttp

/**
* The auth client is built inside `:authlib`, which is KMP and carries no Datadog dependency, so its
* observability is attached here by handing it an engine that is already instrumented.
*
* The hooks sit at the OkHttp layer so resource identity and timing identity come from one place:
* [DatadogInterceptor] opens the RUM resource and [DatadogEventListener] reports DNS, connect, SSL,
* first-byte and download against that same OkHttp-owned key.
*/
@ContributesTo(AppScope::class)
interface AuthNetworkMetroProviders {
@Provides
@SingleIn(AppScope::class)
@AuthHttpClientEngine
fun provideAuthHttpClientEngine(hedvigBuildConstants: HedvigBuildConstants): HttpClientEngine {
val tracedHosts = listOf(authHost(hedvigBuildConstants))
return OkHttp.create {
config {
eventListenerFactory(DatadogEventListener.Factory())
addInterceptor(
DatadogInterceptor.Builder(tracedHosts)
.setTraceSampler(RateBasedSampler(sampleRate = TRACE_SAMPLE_RATE))
.build(),
)
addNetworkInterceptor(
TracingInterceptor.Builder(tracedHosts)
.setTraceSampler(RateBasedSampler(sampleRate = TRACE_SAMPLE_RATE))
.build(),
)
}
}
}
}

private const val TRACE_SAMPLE_RATE = 100f

internal fun authHost(hedvigBuildConstants: HedvigBuildConstants): String =
hedvigBuildConstants.urlAuthService.removePrefix("https://")
Loading
Loading