diff --git a/app/auth/auth-core-public/build.gradle.kts b/app/auth/auth-core-public/build.gradle.kts index 1715406fcc..17288b3ca6 100644 --- a/app/auth/auth-core-public/build.gradle.kts +++ b/app/auth/auth-core-public/build.gradle.kts @@ -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) diff --git a/app/auth/auth-core-public/src/main/kotlin/com/hedvig/android/auth/di/AuthMetroProviders.kt b/app/auth/auth-core-public/src/main/kotlin/com/hedvig/android/auth/di/AuthMetroProviders.kt index 1bc7586217..2873b29c4f 100644 --- a/app/auth/auth-core-public/src/main/kotlin/com/hedvig/android/auth/di/AuthMetroProviders.kt +++ b/app/auth/auth-core-public/src/main/kotlin/com/hedvig/android/auth/di/AuthMetroProviders.kt @@ -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, ) } diff --git a/app/authlib/build.gradle.kts b/app/authlib/build.gradle.kts index 112ef95585..21529527b8 100644 --- a/app/authlib/build.gradle.kts +++ b/app/authlib/build.gradle.kts @@ -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) } diff --git a/app/authlib/src/commonMain/kotlin/com/hedvig/android/authlib/NetworkAuthRepository.kt b/app/authlib/src/commonMain/kotlin/com/hedvig/android/authlib/NetworkAuthRepository.kt index 11ac8a79fe..e579b1ab73 100644 --- a/app/authlib/src/commonMain/kotlin/com/hedvig/android/authlib/NetworkAuthRepository.kt +++ b/app/authlib/src/commonMain/kotlin/com/hedvig/android/authlib/NetworkAuthRepository.kt @@ -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, + engine: HttpClientEngine?, ) : AuthRepository { - private val ktorClient: HttpClient = buildKtorClient(additionalHttpHeadersProvider) + public constructor( + environment: AuthEnvironment, + additionalHttpHeadersProvider: () -> Map, + ) : this(environment, additionalHttpHeadersProvider, null) + + private val ktorClient: HttpClient = buildKtorClient(additionalHttpHeadersProvider, engine) private val authService = AuthService(environment, ktorClient) diff --git a/app/authlib/src/commonMain/kotlin/com/hedvig/android/authlib/internal/KtorConfiguration.kt b/app/authlib/src/commonMain/kotlin/com/hedvig/android/authlib/internal/KtorConfiguration.kt index 3ac2819428..4a87bec744 100644 --- a/app/authlib/src/commonMain/kotlin/com/hedvig/android/authlib/internal/KtorConfiguration.kt +++ b/app/authlib/src/commonMain/kotlin/com/hedvig/android/authlib/internal/KtorConfiguration.kt @@ -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 @@ -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): HttpClient { +internal fun buildKtorClient( + additionalHttpHeadersProvider: () -> Map, + 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() + } } } diff --git a/app/authlib/src/jvmMain/kotlin/com/hedvig/android/authlib/NetworkAuthRepositoryJvm.kt b/app/authlib/src/jvmMain/kotlin/com/hedvig/android/authlib/NetworkAuthRepositoryJvm.kt new file mode 100644 index 0000000000..b84fb11488 --- /dev/null +++ b/app/authlib/src/jvmMain/kotlin/com/hedvig/android/authlib/NetworkAuthRepositoryJvm.kt @@ -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, + engine: HttpClientEngine, +): AuthRepository = NetworkAuthRepository(environment, additionalHttpHeadersProvider, engine) diff --git a/app/authlib/src/jvmTest/kotlin/com/hedvig/android/authlib/NetworkAuthRepositoryEngineTest.kt b/app/authlib/src/jvmTest/kotlin/com/hedvig/android/authlib/NetworkAuthRepositoryEngineTest.kt new file mode 100644 index 0000000000..ca6951791e --- /dev/null +++ b/app/authlib/src/jvmTest/kotlin/com/hedvig/android/authlib/NetworkAuthRepositoryEngineTest.kt @@ -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") + } +} diff --git a/app/core/core-common-public/src/commonMain/kotlin/com/hedvig/android/core/common/di/Qualifiers.kt b/app/core/core-common-public/src/commonMain/kotlin/com/hedvig/android/core/common/di/Qualifiers.kt index 4f4bb33a12..c00a50c008 100644 --- a/app/core/core-common-public/src/commonMain/kotlin/com/hedvig/android/core/common/di/Qualifiers.kt +++ b/app/core/core-common-public/src/commonMain/kotlin/com/hedvig/android/core/common/di/Qualifiers.kt @@ -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 diff --git a/app/datadog/datadog-android/build.gradle.kts b/app/datadog/datadog-android/build.gradle.kts index 2b4ebe8f3c..e6a0f3dd9e 100644 --- a/app/datadog/datadog-android/build.gradle.kts +++ b/app/datadog/datadog-android/build.gradle.kts @@ -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) diff --git a/app/datadog/datadog-android/src/main/kotlin/com/hedvig/android/datadog/core/DatadogInitializer.kt b/app/datadog/datadog-android/src/main/kotlin/com/hedvig/android/datadog/core/DatadogInitializer.kt index b78a18822c..c360baef7d 100644 --- a/app/datadog/datadog-android/src/main/kotlin/com/hedvig/android/datadog/core/DatadogInitializer.kt +++ b/app/datadog/datadog-android/src/main/kotlin/com/hedvig/android/datadog/core/DatadogInitializer.kt @@ -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 @@ -40,7 +41,12 @@ abstract class DatadogInitializer : Initializer { 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) { diff --git a/app/datadog/datadog-android/src/main/kotlin/com/hedvig/android/datadog/core/di/AuthNetworkMetroProviders.kt b/app/datadog/datadog-android/src/main/kotlin/com/hedvig/android/datadog/core/di/AuthNetworkMetroProviders.kt new file mode 100644 index 0000000000..62cc2cbf01 --- /dev/null +++ b/app/datadog/datadog-android/src/main/kotlin/com/hedvig/android/datadog/core/di/AuthNetworkMetroProviders.kt @@ -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://") diff --git a/docs/plans/2026-08-27-datadog-android-metric-recovery.md b/docs/plans/2026-08-27-datadog-android-metric-recovery.md index b7f5a5f797..88a59bfefe 100644 --- a/docs/plans/2026-08-27-datadog-android-metric-recovery.md +++ b/docs/plans/2026-08-27-datadog-android-metric-recovery.md @@ -1,8 +1,10 @@ # Datadog Android metric recovery -**Status: partially complete. One scheduled follow-up is blocked on an app release.** +**Status: three items open.** The `OR`-branch cleanup is blocked on the pre-14.3.6 install base +draining, which was still about 13% of prod view traffic on 2026-09-10. The +claim-submission-failure action is unstarted. The guard-rail monitor is also still just a suggestion. -Last updated 2026-08-28. +Last updated 2026-09-10. ## What broke @@ -29,29 +31,13 @@ ingestion and are not retroactive. ### App code -`Navigation3TrackingEffect` (from `dd-sdk-android-compose`, already pinned) now reports the top of the -back stack as a RUM view, wired in `HedvigApp` off `Backstack.entries`. New view names are the nav key -canonical class names, with no `/{arg}` placeholder suffix: +`Navigation3TrackingEffect` now reports the top of the back stack as a RUM view, wired in `HedvigApp` +off `Backstack.entries`, so view names are nav key canonical class names again. The same change fixed +a pre-existing Firebase defect where `screenName` was `simpleName.removeSuffix("Key")` and silently +merged four pairs of screens sharing a simple name, and it consolidated five KMP modules onto the +`com.hedvig.android.*` namespace that the new naming depends on. -``` -com.hedvig.android.feature.claim.chat.navigation.ClaimOutcomeNewClaimKey -``` - -The same change fixed a pre-existing Firebase defect found along the way: `screenName` was -`simpleName.removeSuffix("Key")`, which silently merged four pairs of screens that share a simple name -across features (`FirstVet`, `Forever`, `SubmitSuccess`, `SubmitFailure`). - -A Firebase screen name is now the key's fully qualified class name with the shared feature package -prefix removed, so `change.tier.navigation.SubmitSuccessKey`. Prepending `com.hedvig.android.feature.` -gets back to the declaration, which means a name read off a Firebase report greps straight to its key -with no convention to decode. The prefix is dropped because GA4 truncates parameter values at 100 -characters and the longest key name is already 91. `ScreenNameTest` asserts uniqueness, reversibility -and that length bound across every key on the classpath. - -That single prefix only works because the package namespace was consolidated at the same time: five -KMP modules (`authlib`, `audio-player-data`, `ui-tiers-and-addons`, `feature-claim-chat`, -`feature-remove-addons`) declared `com.hedvig.*` while every other module and, crucially, every -generated Android namespace used `com.hedvig.android.*`. Those five now match the rest. +Full reasoning, measurements and the `ScreenNameTest` invariants are in PR #3104. ### Datadog: 10 filter rewrites (applied 2026-08-27) @@ -96,7 +82,7 @@ pup rum aggregate \ --compute count --group-by @view.name --limit 120 --from 30d ``` -### The 8 metrics to edit, with their target filters +### The 6 metrics to edit, with their target filters Apply with `pup rum metrics update --file payload.json`, where the payload is: @@ -140,18 +126,32 @@ Apply with `pup rum metrics update --file payload.json`, where the payload @application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:com.hedvig.android.feature.claim.chat.navigation.* @error.source:network @connectivity.status:connected -@error.stack:java.net.ConnectException* -@error.stack:java.net.SocketException* -@error.stack:java.net.SocketTimeoutException* -@error.stack:java.net.UnknownHostException* -@error.stack:java.util.concurrent.CancellationException* ``` -#### `android.login.network.count` +#### The two login metrics are not in this list -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:com.hedvig.android.feature.login.navigation.SwedishLoginKey @connectivity.status:connected -``` +**Superseded 2026-09-09. Do not put a `@view.name` filter on `android.login.network.count` or +`android.login.network.error`.** -#### `android.login.network.error` +Both were rebuilt on auth resource events and no longer mention `@view.name` at all. Their live +filters are: ``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:com.hedvig.android.feature.login.navigation.SwedishLoginKey @connectivity.status:connected -@error.stack:java.net.ConnectException* -@error.stack:java.net.SocketException* -@error.stack:java.net.SocketTimeoutException* -@error.stack:java.net.UnknownHostException* -@error.stack:java.util.concurrent.CancellationException* -@error.stack:*CertPathValidatorException* -@error.message:*CertPathValidatorException* -@error.message:*Connection\ reset* +count @application.id:4d7b8355-396d-406e-b543-30a073050e8f @resource.url_host:(auth.prod.hedvigit.com OR auth.dev.hedvigit.com) @resource.url_path:"/member-login" @session.type:user +error the same, plus @resource.status_code:[500 TO 599] ``` +Both are `event_type: resource` grouped by `env`, so the failure count is a subset of the attempt +count by construction. SLO `29588e73473d54f09814173755548b80` moved to a 30-day window and monitor +`93408872` follows it. + +Applying a view-name filter here would put the denominator back to counting `apollo-router` calls +that merely coincided with the login screen being open. That is the defect that made this SLO report +534.351% of its error budget with no outage behind it: 96 of its 131 denominator events were GraphQL +traffic, and the numerator counted a different event type entirely. + +Note for whoever does the remaining six: `event_type` cannot be changed with `pup rum metrics update`. +The PATCH returns 200, applies the filter and silently discards the event type. A change of event +type needs a delete and recreate under the same name, which does not purge the existing timeseries. + #### `android.changeaddress.view.count` ``` @@ -177,57 +177,35 @@ on top of `Chat` (8,089), inflating the chat metric and the Chat (Android) SLO d 10%. Login has the same hazard, where the wildcard would add `LoginKey`, `OtpInputKey` and `GenericAuthCredentialsInputKey`. Both must keep an explicit single-name filter. -## PENDING: 8 deletions, deliberately held +## Done: 8 dead metrics deleted 2026-09-10 -These metrics read zero and cannot be repaired, because they target screens that no longer exist. -All were confirmed to have **no** dashboards, monitors, SLOs or notebooks attached. `android.claim.failure` -joined this list on 2026-08-27, when the dashboard tile that was its only consumer was replaced (see -the claim-failure section below). +Eight metrics targeted screens deleted in March 2026, or 2023 in the case of `android.auth.failure`, +and had read zero ever since. They were held back only because nobody knew whether deleting a +generated metric also purges its already-computed timeseries. -| Metric | Zero since | Why unrepairable | -|---|---|---| -| `android.claim.singleitempayout` | 2026-03 | No payout step in a chat-based flow | -| `android.claim.submitclaim` | 2026-03 | Keyed on the deleted Summary screen's `@view.url` | -| `android.claimsummary.network.count` | 2026-03 | Same deleted Summary screen | -| `android.claimsummary.network.error` | 2026-03 | Same deleted Summary screen | -| `android.resource.claimflow` | 2026-03 | Duplicate of `android.claimflow.network.count` | -| `android.claimflow.errors` | 2026-03 | Duplicate of `android.claimflow.network.error` | -| `android.auth.failure` | 2023-10 | Matches a hand-written `"BankId Error"` view removed in 2023 | -| `android.claim.failure` | 2026-03 | Targets `ClaimFlowDestination.Failure`; the chat flow has no failure screen | +It does not. Verified 2026-09-09, when `android.login.network.error` was deleted and recreated under +the same name and kept all 24 of its points. These eight had no data to lose either way. -**Why held:** neither the Datadog product docs nor the API reference state whether deleting a -generated metric also purges the already-computed timeseries. Holding costs nothing, since these -metrics already compute zero, so the only correct move was to wait for a definitive answer rather than -risk pre-March history. Resolve by asking Datadog support, then delete. - -Their full definitions are recorded in this repo's git history via this document's companion tooling -output; if any is deleted and needs restoring, recreate with `pup rum metrics create`. +Deleted: `android.claim.singleitempayout`, `android.claim.submitclaim`, +`android.claimsummary.network.count`, `android.claimsummary.network.error`, +`android.resource.claimflow`, `android.claimflow.errors`, `android.auth.failure` and +`android.claim.failure`. Confirmed first that no dashboard, notebook, monitor or SLO referenced any +of them. Their full definitions are recorded in the message of the commit that deleted them, since +Datadog keeps no history of a generated metric's definition. ## Claim-failure signal: replaced 2026-08-27 -`android.claim.failure` targeted `ClaimFlowDestination.Failure`, deleted in March, and there is no -equivalent screen to repoint it at. `ClaimIntentOutcome` is a sealed interface with exactly one case, -`Claim`. Failure surfaces as `ClaimChatUiState.FailedToStart` rendering an error section *inside* the -`ClaimChatKey` view, so it produces no distinct view name and no filter can reach it. - -Rather than count failures, the flow is now measured as a completion ratio: +`android.claim.failure` targeted `ClaimFlowDestination.Failure`, deleted in March. Failure now +surfaces as `ClaimChatUiState.FailedToStart` rendering inside the `ClaimChatKey` view, so it produces +no distinct view name and no filter can reach it. Rather than count failures, the flow is measured as +a completion ratio: `android.claim.started` counts claim-chat entry views, and the dashboard tile +"Claim submissions per chat entry" computes `android.claim.success / android.claim.started * 100`. +Both halves are structurally identical metrics, so counting semantics apply equally to each. -- **`android.claim.started`** created, counting views of the claim chat entry screen, accepting both - the old and new names exactly like `android.claim.success`. -- The dashboard tile "Failure claim screen viewed" was replaced with **"Claim submissions per chat - entry"**, computing `android.claim.success / android.claim.started * 100`. - -Both halves of the ratio are structurally identical metrics (view count, grouped by `env`, same -`uniqueness`), so any counting semantics apply equally to numerator and denominator. - -**Read this number as a trend, not an absolute conversion rate.** The denominator counts chat-screen -views, and a member who backs out and resumes, or returns to a claim later, produces more than one. -On the 30 days before the change the old-name equivalents were 305 chat views against 59 outcome -views, so expect a figure in that region rather than a true per-attempt success rate. What matters is -that it moves when claim submission degrades. - -Because that tile was `android.claim.failure`'s only consumer, that metric now has none, and it joins -the held-deletion list above. +**Read that tile as a trend, not a conversion rate.** The denominator counts chat-screen views, and a +member who backs out and resumes produces more than one. Over the 30 days before the change the +old-name equivalents were 305 chat views against 59 outcome views, so expect a figure in that region. +What matters is that it moves when claim submission degrades. ### Still worth doing: emit an action for submission failure @@ -267,81 +245,12 @@ of ten weeks later. Equivalently, a monitor on distinct `@view.name` cardinality ## Rollback: the filters as they were before 2026-08-27 -Recorded here because these values live nowhere else. They were never in the repo, and Datadog -keeps no history of a generated metric's definition. If a rewrite needs undoing, paste the value -below back with `pup rum metrics update --file payload.json`, same payload shape as above. - -10 metrics were rewritten. Note the rollback is **not** simply "delete the new half of -the `OR`": three of these also dropped a dead branch or widened a wildcard, so the original text is -the only reliable source. - -#### `android.changeaddress.view.count` - -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:"com.hedvig.android.feature.changeaddress.navigation.ChangeAddressDestination.AddressResult?movingDate={movingDate}" -``` - -#### `android.chat.network.count` - -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:"com.hedvig.android.feature.chat.navigation.ChatDestinations.Chat/{conversationId}" @connectivity.status:connected -``` - -#### `android.chat.network.errors` - -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:"com.hedvig.android.feature.chat.navigation.ChatDestinations.Chat/{conversationId}" @connectivity.status:connected -@error.stack:java.net.ConnectException* -@error.stack:java.net.SocketException* -@error.stack:java.net.SocketTimeoutException* -@error.stack:java.net.UnknownHostException* -@error.stack:java.util.concurrent.CancellationException* -``` - -#### `android.claim.success` - -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:(com.hedvig.android.data.claimflow.ClaimFlowDestination.ClaimSuccess OR com.hedvig.feature.claim.chat.ClaimOutcomeNewClaimDestination*) -``` - -#### `android.claimflow.network.count` - -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:(com.hedvig.feature.claim.chat.ClaimChatDestination* OR com.hedvig.android.data.claimflow.ClaimFlowDestination*) @connectivity.status:connected -``` - -#### `android.claimflow.network.error` - -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:(com.hedvig.feature.claim.chat.ClaimChatDestination* OR com.hedvig.android.data.claimflow.ClaimFlowDestination*) @error.source:network @connectivity.status:connected -@error.stack:java.net.ConnectException* -@error.stack:java.net.SocketException* -@error.stack:java.net.SocketTimeoutException* -@error.stack:java.net.UnknownHostException* -@error.stack:java.util.concurrent.CancellationException* -``` - -#### `android.login.network.count` +All ten pre-change definitions were recorded in commit `9e5525fc27`, so they are recoverable with: ``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:com.hedvig.android.feature.login.navigation.LoginDestinations.SwedishLogin @connectivity.status:connected +git show 9e5525fc27:docs/plans/2026-08-27-datadog-android-metric-recovery.md ``` -#### `android.login.network.error` - -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:com.hedvig.android.feature.login.navigation.LoginDestinations.SwedishLogin @connectivity.status:connected -@error.stack:java.net.ConnectException* -@error.stack:java.net.SocketException* -@error.stack:java.net.SocketTimeoutException* -@error.stack:java.net.UnknownHostException* -@error.stack:java.util.concurrent.CancellationException* -@error.stack:*CertPathValidatorException* -@error.message:*CertPathValidatorException* -@error.message:*Connection\ reset* -``` - -#### `android.terminateinsurance.network.count` - -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:com.hedvig.android.feature.terminateinsurance.navigation.TerminateInsuranceDestination* -``` - -#### `android.terminateinsurance.network.error` - -``` -@application.id:4d7b8355-396d-406e-b543-30a073050e8f @view.name:com.hedvig.android.feature.terminateinsurance.navigation.TerminateInsuranceDestination* -@error.type:java.io.IOException @connectivity.status:connected @error.source:network -@error.stack:java.net.SocketTimeoutException* -@error.stack:java.net.ConnectException* -``` - -### Created, so rollback is deletion - -- `android.claim.started` (`pup rum metrics delete android.claim.started`) - -### Dashboard - -On "Apps (Android + iOS)" (`tf2-8n6-9nn`), widget `1151568178197062` was a `query_value` titled -**"Failure claim screen viewed"** reading `sum:android.claim.failure{env:prod}.as_count()`. It now -shows "Claim submissions per chat entry". Restoring it means putting that single query back; no -other widget was touched. +They were written into this document because Datadog keeps no history of a generated metric's +definition. Committing it satisfied that, so the values no longer need to sit in the living copy. +Note that the two login entries there are doubly superseded: they were rebuilt again on 2026-09-09. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef82d6a592..6025f65183 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -166,6 +166,7 @@ datadog-sdk-compose = { module = "com.datadoghq:dd-sdk-android-compose", version datadog-sdk-core = { module = "com.datadoghq:dd-sdk-android-core", version.ref = "datadog" } datadog-sdk-ktor = { module = "com.datadoghq:dd-sdk-kotlin-multiplatform-ktor3", version.ref = "datadogKtor" } datadog-sdk-logs = { module = "com.datadoghq:dd-sdk-android-logs", version.ref = "datadog" } +datadog-sdk-okhttp = { module = "com.datadoghq:dd-sdk-android-okhttp", version.ref = "datadog" } datadog-sdk-rum = { module = "com.datadoghq:dd-sdk-android-rum", version.ref = "datadog" } datadog-sdk-trace-otel = { module = "com.datadoghq:dd-sdk-android-trace-otel", version.ref = "datadog" } firebase-analytics = { module = "com.google.firebase:firebase-analytics" } @@ -215,6 +216,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "media3" }