Skip to content

Commit

Permalink
Add session properties to native crashes from the associated session (#…
Browse files Browse the repository at this point in the history
…1744)

## Goal

Use appState and session properties from associated session when sending native crashes. Prior to this, metadata from the current session was being used erroneously. Now, we will grab this from the session that is associated with the crash that will be resurrected.

In a later PR, the code will be modified to take from the cached payload envelope if a session doesn't exist.
  • Loading branch information
bidetofevil authored Dec 9, 2024
2 parents f3c3d7f + 8f57ff9 commit ac235c4
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ internal fun String.toEmbraceAttributeName(isPrivate: Boolean = false): String {
return prefix + this
}

internal fun String.toSessionPropertyAttributeName(): String = EMBRACE_SESSION_PROPERTY_NAME_PREFIX + this
fun String.toSessionPropertyAttributeName(): String = EMBRACE_SESSION_PROPERTY_NAME_PREFIX + this

fun String.isSessionPropertyAttributeName(): Boolean = startsWith(EMBRACE_SESSION_PROPERTY_NAME_PREFIX)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.embrace.android.embracesdk.internal.ndk

import io.embrace.android.embracesdk.internal.payload.NativeCrashData
import io.opentelemetry.api.common.AttributeKey

/**
* Service to retrieve and delivery native crash data
Expand All @@ -21,7 +22,11 @@ interface NativeCrashService {
/**
* Send the given native crash
*/
fun sendNativeCrash(nativeCrash: NativeCrashData)
fun sendNativeCrash(
nativeCrash: NativeCrashData,
sessionProperties: Map<String, String>,
metadata: Map<AttributeKey<String>, String>,
)

/**
* Delete the data files associated with all the native crashes that have been recorded by the SDK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.embrace.android.embracesdk.internal.payload

import io.embrace.android.embracesdk.internal.arch.schema.EmbType
import io.embrace.android.embracesdk.internal.spans.findAttributeValue
import io.embrace.android.embracesdk.internal.spans.getSessionProperties
import io.embrace.android.embracesdk.internal.spans.hasFixedAttribute
import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes

Expand All @@ -13,3 +14,7 @@ fun Envelope<SessionPayload>.getSessionSpan(): Span? {
fun Envelope<SessionPayload>.getSessionId(): String? {
return getSessionSpan()?.attributes?.findAttributeValue(SessionIncubatingAttributes.SESSION_ID.key)
}

fun Envelope<SessionPayload>.getSessionProperties(): Map<String, String> {
return getSessionSpan()?.getSessionProperties() ?: emptyMap()
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import io.embrace.android.embracesdk.internal.logging.InternalErrorType
import io.embrace.android.embracesdk.internal.ndk.NativeCrashService
import io.embrace.android.embracesdk.internal.opentelemetry.embCrashId
import io.embrace.android.embracesdk.internal.opentelemetry.embHeartbeatTimeUnixNano
import io.embrace.android.embracesdk.internal.opentelemetry.embState
import io.embrace.android.embracesdk.internal.payload.Attribute
import io.embrace.android.embracesdk.internal.payload.Envelope
import io.embrace.android.embracesdk.internal.payload.NativeCrashData
import io.embrace.android.embracesdk.internal.payload.SessionPayload
import io.embrace.android.embracesdk.internal.payload.Span
import io.embrace.android.embracesdk.internal.payload.getSessionId
import io.embrace.android.embracesdk.internal.payload.getSessionProperties
import io.embrace.android.embracesdk.internal.payload.getSessionSpan
import io.embrace.android.embracesdk.internal.payload.toFailedSpan
import io.embrace.android.embracesdk.internal.serialization.PlatformSerializer
Expand All @@ -36,74 +38,97 @@ internal class PayloadResurrectionServiceImpl(

override fun resurrectOldPayloads(nativeCrashServiceProvider: Provider<NativeCrashService?>) {
val nativeCrashService = nativeCrashServiceProvider()
val nativeCrashes = nativeCrashService
?.getNativeCrashes()
?.associateBy { it.sessionId }
?.apply {
values.forEach { nativeCrash ->
nativeCrashService.sendNativeCrash(nativeCrash)
}
} ?: emptyMap()
val undeliveredPayloads = cacheStorageService.getUndeliveredPayloads()
val nativeCrashes = nativeCrashService?.getNativeCrashes()?.associateBy { it.sessionId } ?: emptyMap()
val processedCrashes = mutableSetOf<NativeCrashData>()

cacheStorageService
.getUndeliveredPayloads()
.forEach { deadProcessPayloadMetadata ->
deadProcessPayloadMetadata.processUndeliveredPayload(nativeCrashes::get)
undeliveredPayloads.forEach { payload ->
val result = runCatching {
payload.processUndeliveredPayload(
nativeCrashService = nativeCrashService,
nativeCrashProvider = nativeCrashes::get,
postNativeCrashCallback = processedCrashes::add,
)
}
nativeCrashService?.deleteAllNativeCrashes()
}

/**
* Load and modify the given incomplete payload envelope and send the result to the [IntakeService] for delivery.
* Resurrected payloads sent to the [IntakeService] will be deleted.
*/
private fun StoredTelemetryMetadata.processUndeliveredPayload(nativeCrashProvider: (String) -> NativeCrashData?) {
val result = runCatching {
val resurrectedPayload = when (envelopeType) {
SupportedEnvelopeType.SESSION -> {
val deadSession = serializer.fromJson<Envelope<SessionPayload>>(
inputStream = GZIPInputStream(cacheStorageService.loadPayloadAsStream(this)),
type = envelopeType.serializedType
)

val nativeCrash = deadSession.getSessionId()?.run {
nativeCrashProvider(this)
}
if (result.isSuccess) {
cacheStorageService.delete(payload)
} else {
val exception = IllegalStateException(
"Resurrecting and sending incomplete payloads from previous app launches failed.",
result.exceptionOrNull()
)

deadSession.resurrectSession(nativeCrash)
?: throw IllegalArgumentException(
"Session resurrection failed. Payload does not contain exactly one session span."
)
}
else -> null
logger.trackInternalError(
type = InternalErrorType.PAYLOAD_RESURRECTION_FAIL,
throwable = exception
)
}
}

if (resurrectedPayload != null) {
intakeService.take(
intake = resurrectedPayload,
metadata = copy(complete = true)
if (nativeCrashService != null) {
nativeCrashes.values.filterNot { processedCrashes.contains(it) }.forEach { nativeCrash ->
nativeCrashService.sendNativeCrash(
nativeCrash = nativeCrash,
sessionProperties = emptyMap(),
metadata = emptyMap()
)
}
nativeCrashService.deleteAllNativeCrashes()
}
}

if (result.isSuccess) {
cacheStorageService.delete(this)
} else {
val exception = IllegalStateException(
"Resurrecting and sending incomplete payloads from previous app launches failed.",
result.exceptionOrNull()
)
private fun StoredTelemetryMetadata.processUndeliveredPayload(
nativeCrashService: NativeCrashService?,
nativeCrashProvider: (String) -> NativeCrashData?,
postNativeCrashCallback: (NativeCrashData) -> Unit,
) {
val resurrectedPayload = when (envelopeType) {
SupportedEnvelopeType.SESSION -> {
val deadSession = serializer.fromJson<Envelope<SessionPayload>>(
inputStream = GZIPInputStream(cacheStorageService.loadPayloadAsStream(this)),
type = envelopeType.serializedType
)

val sessionId = deadSession.getSessionId()
val appState = deadSession.getSessionSpan()?.attributes?.findAttributeValue(embState.name)
val nativeCrash = if (sessionId != null) {
nativeCrashProvider(sessionId)?.apply {
postNativeCrashCallback(this)
nativeCrashService?.sendNativeCrash(
nativeCrash = this,
sessionProperties = deadSession.getSessionProperties(),
metadata = if (appState != null) {
mapOf(embState.attributeKey to appState)
} else {
emptyMap()
}
)
}
} else {
null
}

deadSession.resurrectSession(nativeCrash)
?: throw IllegalArgumentException(
"Session resurrection failed. Payload does not contain exactly one session span."
)
}

else -> null
}

logger.trackInternalError(
type = InternalErrorType.PAYLOAD_RESURRECTION_FAIL,
throwable = exception
if (resurrectedPayload != null) {
intakeService.take(
intake = resurrectedPayload,
metadata = copy(complete = true)
)
}
}

/**
* Return copy of envelope with a modified set of spans to reflect their resurrected states, or null if payload does not contain
* exactly one session span.
* Return copy of envelope with a modified set of spans to reflect their resurrected states, or null if the
* payload does not contain exactly one session span.
*/
private fun Envelope<SessionPayload>.resurrectSession(
nativeCrashData: NativeCrashData?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.embrace.android.embracesdk.internal.config.instrumented.InstrumentedCo
import io.embrace.android.embracesdk.internal.config.instrumented.isAttributeValid
import io.embrace.android.embracesdk.internal.config.instrumented.schema.OtelLimitsConfig
import io.embrace.android.embracesdk.internal.payload.Attribute
import io.embrace.android.embracesdk.internal.payload.Span
import io.embrace.android.embracesdk.internal.payload.SpanEvent
import io.embrace.android.embracesdk.internal.utils.isBlankish
import io.embrace.android.embracesdk.spans.EmbraceSpanEvent
Expand Down Expand Up @@ -54,7 +55,7 @@ fun AttributesBuilder.fromMap(
return this
}

fun io.embrace.android.embracesdk.internal.payload.Span.hasFixedAttribute(fixedAttribute: FixedAttribute): Boolean {
fun Span.hasFixedAttribute(fixedAttribute: FixedAttribute): Boolean {
return fixedAttribute.value == attributes?.singleOrNull { it.key == fixedAttribute.key.name }?.data
}

Expand All @@ -70,10 +71,14 @@ fun SpanEvent.hasFixedAttribute(fixedAttribute: FixedAttribute): Boolean =

fun List<Attribute>.findAttributeValue(key: String): String? = singleOrNull { it.key == key }?.data

fun io.embrace.android.embracesdk.internal.payload.Span.getSessionProperty(key: String): String? {
fun Span.getSessionProperty(key: String): String? {
return attributes?.findAttributeValue(key.toSessionPropertyAttributeName())
}

@Suppress("UNCHECKED_CAST")
fun Span.getSessionProperties(): Map<String, String> =
attributes?.filter { it.key != null && it.data != null }?.associate { it.key to it.data } as Map<String, String>

fun Map<String, String>.hasFixedAttribute(fixedAttribute: FixedAttribute): Boolean =
this[fixedAttribute.key.name] == fixedAttribute.value

Expand All @@ -92,7 +97,7 @@ private fun String.isValidLongValueAttribute(): Boolean = longValueAttributes.co

private val longValueAttributes: Set<String> = setOf(ExceptionAttributes.EXCEPTION_STACKTRACE.key)

fun StatusCode.toStatus(): io.embrace.android.embracesdk.internal.payload.Span.Status {
fun StatusCode.toStatus(): Span.Status {
return when (this) {
StatusCode.UNSET -> io.embrace.android.embracesdk.internal.payload.Span.Status.UNSET
StatusCode.OK -> io.embrace.android.embracesdk.internal.payload.Span.Status.OK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig
import io.embrace.android.embracesdk.fakes.config.FakeRedactionConfig
import io.embrace.android.embracesdk.fakes.createSessionBehavior
import io.embrace.android.embracesdk.internal.arch.schema.EmbType
import io.embrace.android.embracesdk.internal.arch.schema.toSessionPropertyAttributeName
import io.embrace.android.embracesdk.internal.config.behavior.REDACTED_LABEL
import io.embrace.android.embracesdk.internal.config.behavior.SensitiveKeysBehaviorImpl
import io.embrace.android.embracesdk.internal.config.remote.RemoteConfig
Expand Down Expand Up @@ -83,7 +84,7 @@ internal class EmbraceLogServiceTest {
// then the telemetry attributes are set correctly
val log = fakeLogWriter.logEvents.single()
val attributes = log.schemaType.attributes()
assertEquals("someValue", attributes["emb.properties.someProperty"])
assertEquals("someValue", attributes["someProperty".toSessionPropertyAttributeName()])
assertTrue(attributes.containsKey(LogIncubatingAttributes.LOG_RECORD_UID.key))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.embrace.android.embracesdk.internal.resurrection

import io.embrace.android.embracesdk.assertions.assertEmbraceSpanData
import io.embrace.android.embracesdk.assertions.findAttributeValue
import io.embrace.android.embracesdk.assertions.findSpansByName
import io.embrace.android.embracesdk.assertions.getLastHeartbeatTimeMs
import io.embrace.android.embracesdk.assertions.getSessionId
Expand All @@ -17,10 +18,12 @@ import io.embrace.android.embracesdk.fakes.fakeIncompleteSessionEnvelope
import io.embrace.android.embracesdk.fixtures.fakeCachedSessionStoredTelemetryMetadata
import io.embrace.android.embracesdk.fixtures.fakeNativeCrashStoredTelemetryMetadata
import io.embrace.android.embracesdk.internal.arch.schema.EmbType
import io.embrace.android.embracesdk.internal.arch.schema.isSessionPropertyAttributeName
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
import io.embrace.android.embracesdk.internal.delivery.StoredTelemetryMetadata
import io.embrace.android.embracesdk.internal.delivery.SupportedEnvelopeType
import io.embrace.android.embracesdk.internal.opentelemetry.embCrashId
import io.embrace.android.embracesdk.internal.opentelemetry.embState
import io.embrace.android.embracesdk.internal.payload.Envelope
import io.embrace.android.embracesdk.internal.payload.NativeCrashData
import io.embrace.android.embracesdk.internal.payload.SessionPayload
Expand Down Expand Up @@ -206,7 +209,8 @@ class PayloadResurrectionServiceImplTest {
val earlierDeadSession = fakeIncompleteSessionEnvelope(
sessionId = "anotherFakeSessionId",
startMs = deadSessionEnvelope.getStartTime() - 100_000L,
lastHeartbeatTimeMs = deadSessionEnvelope.getStartTime() - 90_000L
lastHeartbeatTimeMs = deadSessionEnvelope.getStartTime() - 90_000L,
sessionProperties = mapOf("prop" to "earlier")

)
val earlierSessionCrashData = createNativeCrashData(
Expand Down Expand Up @@ -237,6 +241,10 @@ class PayloadResurrectionServiceImplTest {
"native-crash-1",
envelope.getSessionSpan()?.attributes?.findAttributeValue(embCrashId.name)
)
assertEquals(
"foreground",
envelope.getSessionSpan()?.attributes?.findAttributeValue(embState.name)
)
}

with(sessionPayloads.last()) {
Expand All @@ -246,11 +254,41 @@ class PayloadResurrectionServiceImplTest {
"native-crash-2",
envelope.getSessionSpan()?.attributes?.findAttributeValue(embCrashId.name)
)
assertEquals(
"foreground",
envelope.getSessionSpan()?.attributes?.findAttributeValue(embState.name)
)
}

assertEquals(2, nativeCrashService.nativeCrashesSent.size)
assertEquals(deadSessionCrashData, nativeCrashService.nativeCrashesSent.first())
assertEquals(earlierSessionCrashData, nativeCrashService.nativeCrashesSent.last())
with(nativeCrashService.nativeCrashesSent.first()) {
assertEquals(deadSessionCrashData, first)
assertTrue(second.keys.none { it.isSessionPropertyAttributeName() })
}
with(nativeCrashService.nativeCrashesSent.last()) {
assertEquals(earlierSessionCrashData, first)
assertEquals(
"earlier",
second.findAttributeValue(second.keys.single { it.isSessionPropertyAttributeName() })
)
}
}

@Test
fun `native crashes without sessions are sent properly`() {
val deadSessionCrashData = createNativeCrashData(
nativeCrashId = "native-crash-1",
sessionId = "no-session-id"
)
nativeCrashService.addNativeCrashData(deadSessionCrashData)
resurrectionService.resurrectOldPayloads({ nativeCrashService })

assertEquals(0, intakeService.getIntakes<SessionPayload>().size)
assertEquals(1, nativeCrashService.nativeCrashesSent.size)
with(nativeCrashService.nativeCrashesSent.first()) {
assertEquals(deadSessionCrashData, first)
assertTrue(second.keys.none { it.isSessionPropertyAttributeName() || embState.name == it })
}
}

private fun Envelope<SessionPayload>.resurrectPayload() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ internal class NativeFeatureModuleImpl(
null
} else {
NativeCrashDataSourceImpl(
sessionPropertiesService = essentialServiceModule.sessionPropertiesService,
nativeCrashProcessor = nativeCoreModule.processor,
preferencesService = androidServicesModule.preferencesService,
logWriter = essentialServiceModule.logWriter,
Expand Down
Loading

0 comments on commit ac235c4

Please sign in to comment.