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
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ internal class GooglePlayBillingClient(
metadata = {
"code" to billingResult.responseCode
"message" to billingResult.debugMessage
"purchases" to purchases?.count()
"purchases" to (purchases?.count() ?: -1)
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,10 @@ class NotificationService : FirebaseMessagingService(),
message = "onMessageReceived",
type = TraceType.Process,
metadata = {
"title" to title
"body" to body
// Push content is not recorded: TraceType.Process is forwarded to
// breadcrumb sinks, and message text does not belong in Bugsnag.
"silent" to (title == null)
"has_body" to (body != null)
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,9 @@ class CoinbaseOnRampController @Inject constructor(
"orderId" to orderId
"httpCode" to error.code().toString()
"errorType" to (coinbaseError?.let { it::class.simpleName } ?: "unknown")
"correlationId" to coinbaseError?.correlationId
"responseBody" to errorBody
"errorLink" to coinbaseError?.errorLink
"correlationId" to coinbaseError?.correlationId.orEmpty()
"responseBody" to errorBody.orEmpty()
"errorLink" to coinbaseError?.errorLink.orEmpty()
},
error = error,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1860,7 +1860,7 @@ class SwapViewModel @Inject constructor(
message = "Something went wrong during phantom onramp",
type = TraceType.Error,
metadata = {
"errorMessage" to deeplinkError.message
"errorMessage" to deeplinkError.message.orEmpty()
"code" to deeplinkError.code
},
error = deeplinkError.takeUnless { it.isAlert }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.getcode.opencode.model.accounts.entropy
import com.getcode.opencode.model.financial.LocalFiat
import com.getcode.opencode.model.financial.Token
import com.getcode.solana.keys.Mint
import com.getcode.solana.keys.base58
import com.getcode.utils.trace
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
Expand Down Expand Up @@ -148,7 +149,10 @@ internal class GiftCardFundingWorker @AssistedInject constructor(
tag = "GiftCardFundingWorker",
message = "Successfully funded gift card",
metadata = {
"giftCard" to giftCard.entropy
// The vault address identifies the gift card without
// exposing its entropy, which is the seed controlling
// its funds. This trace reaches breadcrumb sinks.
"giftCardVault" to giftCard.cluster.vaultPublicKey.base58()
}
)
cont.resume(kotlin.Result.success(it))
Expand All @@ -158,7 +162,7 @@ internal class GiftCardFundingWorker @AssistedInject constructor(
tag = "GiftCardFundingWorker",
message = "Failed to fund gift card",
metadata = {
"giftCard" to giftCard.entropy
"giftCardVault" to giftCard.cluster.vaultPublicKey.base58()
},
error = it
)
Expand Down
18 changes: 16 additions & 2 deletions libs/logging/src/main/kotlin/com/getcode/utils/Logging.kt
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,25 @@ suspend fun <T> timedTraceSuspend(
class MetadataBuilder {
private val map = mutableMapOf<String, Any>()

infix fun String.to(value: Any) {
map[this] = value
/**
* Records [value] under this key, substituting [NULL_PLACEHOLDER] when it is `null`.
*
* The parameter is deliberately nullable. A non-null `Any` parameter makes this
* function inapplicable to a nullable argument, so the call silently resolves to
* [kotlin.to] instead, building a [Pair] that is discarded in statement position —
* the field is dropped with no error and no warning. Accepting `Any?` keeps this
* member the only candidate, so every pair is recorded.
*/
infix fun String.to(value: Any?) {
map[this] = value ?: NULL_PLACEHOLDER
}

fun build(): Map<String, Any> = map

companion object {
/** Recorded in place of a `null` metadata value so the field stays visible. */
const val NULL_PLACEHOLDER = "null"
}
}

/** Convenience factory that builds a metadata map from [block]. */
Expand Down
103 changes: 103 additions & 0 deletions libs/logging/src/test/kotlin/com/getcode/utils/MetadataBuilderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.getcode.utils

import kotlin.test.Test
import kotlin.test.assertEquals

/**
* Guards the overload resolution of [MetadataBuilder.to].
*
* When the parameter was a non-null `Any`, a nullable argument made the member
* inapplicable and the call resolved to [kotlin.to] instead — building a [Pair]
* that was discarded in statement position. The field was dropped silently, with
* no error and at most an unused-expression warning. These tests fail if the
* parameter is ever narrowed back to a non-null type.
*/
class MetadataBuilderTest {

private fun build(block: MetadataBuilder.() -> Unit): Map<String, Any> =
MetadataBuilder().apply(block).build()

@Test
fun `nullable String holding a value is recorded`() {
val present: String? = "hello"

val result = build { "value" to present }

assertEquals(mapOf("value" to "hello"), result)
}

@Test
fun `nullable String holding null is recorded as the placeholder`() {
val absent: String? = null

val result = build { "value" to absent }

assertEquals(mapOf("value" to MetadataBuilder.NULL_PLACEHOLDER), result)
}

@Test
fun `both nullable cases appear in the same map`() {
val present: String? = "hello"
val absent: String? = null

val result = build {
"present" to present
"absent" to absent
}

assertEquals(
mapOf("present" to "hello", "absent" to MetadataBuilder.NULL_PLACEHOLDER),
result,
)
}

@Test
fun `non-null values are recorded unchanged`() {
val result = build {
"string" to "text"
"int" to 1
"boolean" to true
}

assertEquals(mapOf("string" to "text", "int" to 1, "boolean" to true), result)
}

@Test
fun `nullable non-String types are recorded`() {
val count: Int? = null
val flag: Boolean? = false

val result = build {
"count" to count
"flag" to flag
}

assertEquals(
mapOf("count" to MetadataBuilder.NULL_PLACEHOLDER, "flag" to false),
result,
)
}

@Test
fun `every pair reaches the map`() {
val absent: String? = null

val result = build {
"a" to "one"
"b" to absent
"c" to 3
}

assertEquals(3, result.size)
}

@Test
fun `a repeated key keeps the last value`() {
val result = build {
"key" to "first"
"key" to "second"
}

assertEquals(mapOf("key" to "second"), result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ internal class RealVerifiedFiatCalculator @Inject constructor(
"original currency fx" to rate.fx
"requested amount" to amount.formatted()
"requested quarks (in USD)" to usdValue.quarks * 1_000_000
"balance quarks (in USD)" to balance?.quarks?.times(1_000_000)
"balance quarks (in USD)" to (balance?.quarks?.times(1_000_000) ?: "none")
"capped quarks (in USD)" to cappedValue.quarks * 1_000_000
"supply of ${token.symbol}" to supply
"calculated quarks" to quarks
Expand Down
Loading