diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/envelope/log/LogEnvelopeSourceImpl.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/envelope/log/LogEnvelopeSourceImpl.kt index 6432e82a74..38cb513fc5 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/envelope/log/LogEnvelopeSourceImpl.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/envelope/log/LogEnvelopeSourceImpl.kt @@ -1,23 +1,31 @@ package io.embrace.android.embracesdk.internal.envelope.log +import io.embrace.android.embracesdk.internal.delivery.PayloadType +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore.Companion.createNativeCrashEnvelopeMetadata import io.embrace.android.embracesdk.internal.envelope.metadata.EnvelopeMetadataSource import io.embrace.android.embracesdk.internal.envelope.resource.EnvelopeResourceSource import io.embrace.android.embracesdk.internal.logs.LogRequest +import io.embrace.android.embracesdk.internal.opentelemetry.embProcessIdentifier import io.embrace.android.embracesdk.internal.payload.Envelope +import io.embrace.android.embracesdk.internal.payload.Envelope.Companion.createLogEnvelope import io.embrace.android.embracesdk.internal.payload.LogPayload +import io.embrace.android.embracesdk.internal.spans.findAttributeValue +import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes internal class LogEnvelopeSourceImpl( private val metadataSource: EnvelopeMetadataSource, private val resourceSource: EnvelopeResourceSource, private val logPayloadSource: LogPayloadSource, + private val cachedLogEnvelopeStore: CachedLogEnvelopeStore?, ) : LogEnvelopeSource { override fun getBatchedLogEnvelope(): Envelope = getLogEnvelope(logPayloadSource.getBatchedLogPayload()) override fun getSingleLogEnvelopes(): List>> { - val payloads = logPayloadSource.getSingleLogPayloads() - return if (payloads.isNotEmpty()) { - payloads.map { LogRequest(payload = getLogEnvelope(it.payload), defer = it.defer) } + val requests = logPayloadSource.getSingleLogPayloads() + return if (requests.isNotEmpty()) { + requests.map { LogRequest(payload = getLogEnvelope(it.payload), defer = it.defer) } } else { emptyList() } @@ -27,11 +35,27 @@ internal class LogEnvelopeSourceImpl( return getLogEnvelope(LogPayload(logs = emptyList())) } - private fun getLogEnvelope(payload: LogPayload) = Envelope( - resourceSource.getEnvelopeResource(), - metadataSource.getEnvelopeMetadata(), - "0.1.0", - "logs", - payload - ) + private fun getLogEnvelope(payload: LogPayload): Envelope { + if (cachedLogEnvelopeStore != null && payload.findType() == PayloadType.NATIVE_CRASH) { + val nativeCrash = payload.logs?.firstOrNull() + val envelope = cachedLogEnvelopeStore.get( + createNativeCrashEnvelopeMetadata( + sessionId = nativeCrash?.attributes?.findAttributeValue(SessionIncubatingAttributes.SESSION_ID.key), + processIdentifier = nativeCrash?.attributes?.findAttributeValue(embProcessIdentifier.name) + ) + ) + + if (envelope != null) { + return envelope.copy(data = payload) + } + } + + return payload.createLogEnvelope( + resource = resourceSource.getEnvelopeResource(), + metadata = metadataSource.getEnvelopeMetadata() + ) + } + + private fun LogPayload.findType(): PayloadType = + PayloadType.fromValue(logs?.firstOrNull()?.attributes?.findAttributeValue("emb.type")) } diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/DeliveryModule.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/DeliveryModule.kt index 9e34114a51..3d335427c9 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/DeliveryModule.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/DeliveryModule.kt @@ -6,6 +6,7 @@ import io.embrace.android.embracesdk.internal.delivery.debug.DeliveryTracer import io.embrace.android.embracesdk.internal.delivery.execution.RequestExecutionService import io.embrace.android.embracesdk.internal.delivery.intake.IntakeService import io.embrace.android.embracesdk.internal.delivery.scheduling.SchedulingService +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore import io.embrace.android.embracesdk.internal.delivery.storage.PayloadStorageService import io.embrace.android.embracesdk.internal.session.orchestrator.PayloadStore @@ -22,6 +23,7 @@ interface DeliveryModule { val payloadCachingService: PayloadCachingService? val payloadStorageService: PayloadStorageService? val cacheStorageService: PayloadStorageService? + val cachedLogEnvelopeStore: CachedLogEnvelopeStore? val requestExecutionService: RequestExecutionService? val schedulingService: SchedulingService? val deliveryTracer: DeliveryTracer? diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/DeliveryModuleImpl.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/DeliveryModuleImpl.kt index aaabf09901..7445e2655c 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/DeliveryModuleImpl.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/DeliveryModuleImpl.kt @@ -14,6 +14,8 @@ import io.embrace.android.embracesdk.internal.delivery.intake.IntakeService import io.embrace.android.embracesdk.internal.delivery.intake.IntakeServiceImpl import io.embrace.android.embracesdk.internal.delivery.scheduling.SchedulingService import io.embrace.android.embracesdk.internal.delivery.scheduling.SchedulingServiceImpl +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStoreImpl import io.embrace.android.embracesdk.internal.delivery.storage.PayloadStorageService import io.embrace.android.embracesdk.internal.delivery.storage.PayloadStorageServiceImpl import io.embrace.android.embracesdk.internal.delivery.storage.StorageLocation @@ -146,6 +148,20 @@ internal class DeliveryModuleImpl( } } + override val cachedLogEnvelopeStore: CachedLogEnvelopeStore? by singleton { + if (configModule.configService.isOnlyUsingOtelExporters()) { + null + } else { + val location = StorageLocation.ENVELOPE.asFile(coreModule.context, initModule.logger) + CachedLogEnvelopeStoreImpl( + outputDir = location, + worker = dataPersistenceWorker, + logger = initModule.logger, + serializer = initModule.jsonSerializer + ) + } + } + override val requestExecutionService: RequestExecutionService? by singleton { requestExecutionServiceProvider?.invoke() ?: if (configModule.configService.isOnlyUsingOtelExporters()) { null diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/InitModule.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/InitModule.kt index d931355be7..4f62f05abb 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/InitModule.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/InitModule.kt @@ -36,4 +36,6 @@ interface InitModule { val jsonSerializer: PlatformSerializer val instrumentedConfig: InstrumentedConfig + + val processIdentifierProvider: () -> String } diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/InitModuleImpl.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/InitModuleImpl.kt index 9502c0c6d7..43eb33216a 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/InitModuleImpl.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/InitModuleImpl.kt @@ -1,5 +1,6 @@ package io.embrace.android.embracesdk.internal.injection +import io.embrace.android.embracesdk.internal.IdGenerator import io.embrace.android.embracesdk.internal.SystemInfo import io.embrace.android.embracesdk.internal.clock.Clock import io.embrace.android.embracesdk.internal.clock.NormalizedIntervalClock @@ -18,6 +19,7 @@ internal class InitModuleImpl( override val clock: Clock = NormalizedIntervalClock(systemClock = SystemClock()), override val logger: EmbLogger = EmbLoggerImpl(), override val systemInfo: SystemInfo = SystemInfo(), + override val processIdentifierProvider: () -> String = IdGenerator.Companion::generateLaunchInstanceId, ) : InitModule { override val telemetryService: TelemetryService by singleton { diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/OpenTelemetryModuleImpl.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/OpenTelemetryModuleImpl.kt index b491c1b1f2..5c353c6070 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/OpenTelemetryModuleImpl.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/OpenTelemetryModuleImpl.kt @@ -40,7 +40,12 @@ internal class OpenTelemetryModuleImpl( } override val openTelemetryConfiguration: OpenTelemetryConfiguration by lazy { - OpenTelemetryConfiguration(spanSink, logSink, initModule.systemInfo) + OpenTelemetryConfiguration( + spanSink = spanSink, + logSink = logSink, + systemInfo = initModule.systemInfo, + processIdentifierProvider = initModule.processIdentifierProvider + ) } private val openTelemetrySdk: OpenTelemetrySdk by lazy { diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/PayloadSourceModuleImpl.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/PayloadSourceModuleImpl.kt index eeb648f83e..77e9119036 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/PayloadSourceModuleImpl.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/PayloadSourceModuleImpl.kt @@ -72,7 +72,7 @@ internal class PayloadSourceModuleImpl( } override val logEnvelopeSource: LogEnvelopeSource by singleton { - LogEnvelopeSourceImpl(metadataSource, resourceSource, logPayloadSource) + LogEnvelopeSourceImpl(metadataSource, resourceSource, logPayloadSource, deliveryModule.cachedLogEnvelopeStore) } override val deviceArchitecture: DeviceArchitecture by singleton { @@ -130,16 +130,20 @@ internal class PayloadSourceModuleImpl( } } + @Suppress("ComplexCondition") override val payloadResurrectionService: PayloadResurrectionService? by singleton { val intakeService = deliveryModule.intakeService val cacheStorageService = deliveryModule.cacheStorageService + val cachedLogEnvelopeStore = deliveryModule.cachedLogEnvelopeStore if (configModule.configService.autoDataCaptureBehavior.isV2StorageEnabled() && intakeService != null && - cacheStorageService != null + cacheStorageService != null && + cachedLogEnvelopeStore != null ) { PayloadResurrectionServiceImpl( intakeService = intakeService, cacheStorageService = cacheStorageService, + cachedLogEnvelopeStore = cachedLogEnvelopeStore, logger = initModule.logger, serializer = initModule.jsonSerializer ) diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/opentelemetry/OpenTelemetryConfiguration.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/opentelemetry/OpenTelemetryConfiguration.kt index 92a13f2348..deab10dec9 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/opentelemetry/OpenTelemetryConfiguration.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/opentelemetry/OpenTelemetryConfiguration.kt @@ -25,6 +25,7 @@ class OpenTelemetryConfiguration( spanSink: SpanSink, logSink: LogSink, systemInfo: SystemInfo, + private val processIdentifierProvider: () -> String = IdGenerator.Companion::generateLaunchInstanceId ) { val embraceSdkName: String = BuildConfig.LIBRARY_PACKAGE_NAME val embraceSdkVersion: String = BuildConfig.VERSION_NAME @@ -49,7 +50,7 @@ class OpenTelemetryConfiguration( * this out by proximity for stitched sessions. */ val processIdentifier: String by lazy { - Systrace.traceSynchronous("process-identifier-init", IdGenerator.Companion::generateLaunchInstanceId) + Systrace.traceSynchronous("process-identifier-init", processIdentifierProvider) } private val externalSpanExporters = mutableListOf() diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/resurrection/PayloadResurrectionServiceImpl.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/resurrection/PayloadResurrectionServiceImpl.kt index 0d96ed44fa..76473bb1b8 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/resurrection/PayloadResurrectionServiceImpl.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/resurrection/PayloadResurrectionServiceImpl.kt @@ -5,15 +5,22 @@ 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.delivery.intake.IntakeService +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore.Companion.createNativeCrashEnvelopeMetadata import io.embrace.android.embracesdk.internal.delivery.storage.PayloadStorageService import io.embrace.android.embracesdk.internal.logging.EmbLogger 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.embProcessIdentifier import io.embrace.android.embracesdk.internal.opentelemetry.embState +import io.embrace.android.embracesdk.internal.payload.ApplicationState import io.embrace.android.embracesdk.internal.payload.Attribute import io.embrace.android.embracesdk.internal.payload.Envelope +import io.embrace.android.embracesdk.internal.payload.EnvelopeMetadata +import io.embrace.android.embracesdk.internal.payload.EnvelopeResource +import io.embrace.android.embracesdk.internal.payload.LogPayload import io.embrace.android.embracesdk.internal.payload.NativeCrashData import io.embrace.android.embracesdk.internal.payload.SessionPayload import io.embrace.android.embracesdk.internal.payload.Span @@ -26,12 +33,14 @@ import io.embrace.android.embracesdk.internal.spans.findAttributeValue import io.embrace.android.embracesdk.internal.spans.hasFixedAttribute import io.embrace.android.embracesdk.internal.utils.Provider import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes +import java.util.Locale import java.util.zip.GZIPInputStream import kotlin.math.max internal class PayloadResurrectionServiceImpl( private val intakeService: IntakeService, private val cacheStorageService: PayloadStorageService, + private val cachedLogEnvelopeStore: CachedLogEnvelopeStore, private val logger: EmbLogger, private val serializer: PlatformSerializer, ) : PayloadResurrectionService { @@ -39,15 +48,16 @@ internal class PayloadResurrectionServiceImpl( override fun resurrectOldPayloads(nativeCrashServiceProvider: Provider) { val nativeCrashService = nativeCrashServiceProvider() val undeliveredPayloads = cacheStorageService.getUndeliveredPayloads() + val payloadsToResurrect = undeliveredPayloads.filterNot { it.isCrashEnvelope() } val nativeCrashes = nativeCrashService?.getNativeCrashes()?.associateBy { it.sessionId } ?: emptyMap() val processedCrashes = mutableSetOf() - undeliveredPayloads.forEach { payload -> + payloadsToResurrect.forEach { payload -> val result = runCatching { payload.processUndeliveredPayload( nativeCrashService = nativeCrashService, nativeCrashProvider = nativeCrashes::get, - postNativeCrashCallback = processedCrashes::add, + postNativeCrashProcessingCallback = processedCrashes::add, ) } @@ -67,21 +77,81 @@ internal class PayloadResurrectionServiceImpl( } if (nativeCrashService != null) { - nativeCrashes.values.filterNot { processedCrashes.contains(it) }.forEach { nativeCrash -> - nativeCrashService.sendNativeCrash( - nativeCrash = nativeCrash, - sessionProperties = emptyMap(), - metadata = emptyMap() - ) + // We assume that there can ever only be one cached crash envelope and one sessionless native crash + // Internal errors will be logged if that assumption is not true, as we currently don't store enough + // metadata in the native crash to determine which app instance it came from if it isn't associated with + // a session. + // + // This assumption would be incorrect if a native crash happens during startup, before a session is created, + // and before the payload resurrection phase of the SDK startup has completed. This seems pretty rare. + // + // Solving this requires the persistence of the processIdentifier, and we will only do this if this + // proves to be a problem in production. + + val sessionlessNativeCrashes = nativeCrashes.values.filterNot { processedCrashes.contains(it) } + if (sessionlessNativeCrashes.isNotEmpty()) { + val cachedCrashEnvelopeMetadata = undeliveredPayloads.firstOrNull { it.isCrashEnvelope() } + val cachedCrashEnvelope = if (cachedCrashEnvelopeMetadata != null) { + runCatching { + serializer.fromJson>( + inputStream = GZIPInputStream( + cacheStorageService.loadPayloadAsStream(cachedCrashEnvelopeMetadata) + ), + type = SupportedEnvelopeType.CRASH.serializedType + ).also { + cacheStorageService.delete(cachedCrashEnvelopeMetadata) + } + }.getOrNull() + } else { + null + } + val resource = cachedCrashEnvelope?.resource + val metadata = cachedCrashEnvelope?.metadata + sessionlessNativeCrashes.forEach { nativeCrash -> + if (resource != null && metadata != null) { + cachedLogEnvelopeStore.create( + storedTelemetryMetadata = createNativeCrashEnvelopeMetadata( + sessionId = nativeCrash.sessionId + ), + resource = resource, + metadata = metadata + ) + } else { + logger.trackInternalError( + type = InternalErrorType.NATIVE_CRASH_RESURRECTION_ERROR, + throwable = IllegalStateException("Cached native crash envelope data not found") + ) + } + nativeCrashService.sendNativeCrash( + nativeCrash = nativeCrash, + sessionProperties = emptyMap(), + metadata = mapOf( + embState.attributeKey to ApplicationState.BACKGROUND.name.lowercase(Locale.ENGLISH) + ), + ) + } + if (sessionlessNativeCrashes.size > 1) { + logger.trackInternalError( + type = InternalErrorType.NATIVE_CRASH_RESURRECTION_ERROR, + throwable = IllegalStateException("Multiple sessionless native crashes found.") + ) + } } nativeCrashService.deleteAllNativeCrashes() } + + undeliveredPayloads.filter { it.isCrashEnvelope() }.forEach { crashEnvelopeMetadata -> + cacheStorageService.delete(crashEnvelopeMetadata) + } + cachedLogEnvelopeStore.clear() } + private fun StoredTelemetryMetadata.isCrashEnvelope() = envelopeType == SupportedEnvelopeType.CRASH + private fun StoredTelemetryMetadata.processUndeliveredPayload( nativeCrashService: NativeCrashService?, nativeCrashProvider: (String) -> NativeCrashData?, - postNativeCrashCallback: (NativeCrashData) -> Unit, + postNativeCrashProcessingCallback: (NativeCrashData) -> Unit, ) { val resurrectedPayload = when (envelopeType) { SupportedEnvelopeType.SESSION -> { @@ -92,18 +162,33 @@ internal class PayloadResurrectionServiceImpl( val sessionId = deadSession.getSessionId() val appState = deadSession.getSessionSpan()?.attributes?.findAttributeValue(embState.name) - val nativeCrash = if (sessionId != null) { + val nativeCrash = if (nativeCrashService != null && sessionId != null) { nativeCrashProvider(sessionId)?.apply { - postNativeCrashCallback(this) - nativeCrashService?.sendNativeCrash( + val nativeCrashEnvelopeMetadata = createNativeCrashEnvelopeMetadata( + sessionId = sessionId, + processIdentifier = processId + ) + + cachedLogEnvelopeStore.create( + storedTelemetryMetadata = nativeCrashEnvelopeMetadata, + resource = deadSession.resource ?: EnvelopeResource(), + metadata = deadSession.metadata ?: EnvelopeMetadata() + ) + + nativeCrashService.sendNativeCrash( nativeCrash = this, sessionProperties = deadSession.getSessionProperties(), metadata = if (appState != null) { - mapOf(embState.attributeKey to appState) + mapOf( + embState.attributeKey to appState, + embProcessIdentifier.attributeKey to processId + ) } else { emptyMap() - } + }, ) + + postNativeCrashProcessingCallback(this) } } else { null diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/session/orchestrator/PayloadStore.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/session/orchestrator/PayloadStore.kt index 8f532150af..7f85f2a917 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/session/orchestrator/PayloadStore.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/session/orchestrator/PayloadStore.kt @@ -1,7 +1,6 @@ package io.embrace.android.embracesdk.internal.session.orchestrator import io.embrace.android.embracesdk.internal.capture.crash.CrashTeardownHandler -import io.embrace.android.embracesdk.internal.delivery.PayloadType import io.embrace.android.embracesdk.internal.payload.Envelope import io.embrace.android.embracesdk.internal.payload.LogPayload import io.embrace.android.embracesdk.internal.payload.SessionPayload @@ -30,7 +29,7 @@ interface PayloadStore : CrashTeardownHandler { fun storeLogPayload(envelope: Envelope, attemptImmediateRequest: Boolean) /** - * Stores an empty envelope for [PayloadType.NATIVE_CRASH] for future use. One one cached version of this should + * Stores an empty payload-type-less crash envelope for future use. One one cached version of this should * exist at one time. */ fun cacheEmptyCrashEnvelope(envelope: Envelope) diff --git a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/session/orchestrator/V2PayloadStore.kt b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/session/orchestrator/V2PayloadStore.kt index 9208a971cc..805d2072b7 100644 --- a/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/session/orchestrator/V2PayloadStore.kt +++ b/embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/session/orchestrator/V2PayloadStore.kt @@ -47,7 +47,7 @@ internal class V2PayloadStore( metadata = createMetadata( type = SupportedEnvelopeType.CRASH, complete = false, - payloadType = PayloadType.NATIVE_CRASH + payloadType = PayloadType.UNKNOWN ) ) } diff --git a/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/comms/delivery/EmbraceDeliveryServiceTest.kt b/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/comms/delivery/EmbraceDeliveryServiceTest.kt index 9588758a7f..6533c1a748 100644 --- a/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/comms/delivery/EmbraceDeliveryServiceTest.kt +++ b/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/comms/delivery/EmbraceDeliveryServiceTest.kt @@ -24,6 +24,7 @@ import io.embrace.android.embracesdk.internal.logging.EmbLogger import io.embrace.android.embracesdk.internal.logging.EmbLoggerImpl import io.embrace.android.embracesdk.internal.opentelemetry.embCrashId import io.embrace.android.embracesdk.internal.payload.Envelope +import io.embrace.android.embracesdk.internal.payload.Envelope.Companion.createLogEnvelope import io.embrace.android.embracesdk.internal.payload.Log import io.embrace.android.embracesdk.internal.payload.LogPayload import io.embrace.android.embracesdk.internal.payload.NativeCrashData @@ -337,17 +338,9 @@ internal class EmbraceDeliveryServiceTest { incompleteSessionEnvelope.getSessionId(), incompleteSessionEnvelope.getStartTime(), ).filename - private val logsEnvelope = Envelope( + private val logsEnvelope = LogPayload(logs = listOf(Log(), Log())).createLogEnvelope( resource = FakeEnvelopeResourceSource().resource, metadata = FakeEnvelopeMetadataSource().metadata, - version = "0.1.0", - type = "logs", - data = LogPayload( - logs = listOf( - Log(), - Log() - ) - ) ) } } diff --git a/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/envelope/log/LogEnvelopeSourceImplTest.kt b/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/envelope/log/LogEnvelopeSourceImplTest.kt index fb04bced9b..44ad2c56b6 100644 --- a/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/envelope/log/LogEnvelopeSourceImplTest.kt +++ b/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/envelope/log/LogEnvelopeSourceImplTest.kt @@ -1,11 +1,23 @@ package io.embrace.android.embracesdk.internal.envelope.log +import io.embrace.android.embracesdk.fakes.FakeCachedLogEnvelopeStore import io.embrace.android.embracesdk.fakes.FakeEnvelopeMetadataSource import io.embrace.android.embracesdk.fakes.FakeEnvelopeResourceSource import io.embrace.android.embracesdk.fakes.FakeLogPayloadSource +import io.embrace.android.embracesdk.fakes.fakeEnvelopeMetadata +import io.embrace.android.embracesdk.fakes.fakeEnvelopeResource +import io.embrace.android.embracesdk.fixtures.nativeCrashLog +import io.embrace.android.embracesdk.fixtures.nativeCrashWithoutSessionLog import io.embrace.android.embracesdk.fixtures.sendImmediatelyLog +import io.embrace.android.embracesdk.fixtures.testLog +import io.embrace.android.embracesdk.internal.delivery.PayloadType +import io.embrace.android.embracesdk.internal.delivery.SupportedEnvelopeType +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore.Companion.createNativeCrashEnvelopeMetadata import io.embrace.android.embracesdk.internal.logs.LogRequest +import io.embrace.android.embracesdk.internal.opentelemetry.embProcessIdentifier import io.embrace.android.embracesdk.internal.payload.LogPayload +import io.embrace.android.embracesdk.internal.spans.findAttributeValue +import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before @@ -13,34 +25,35 @@ import org.junit.Test internal class LogEnvelopeSourceImplTest { + private val fakeBatchedPayload = LogPayload(logs = listOf(testLog)) private lateinit var metadataSource: FakeEnvelopeMetadataSource private lateinit var resourceSource: FakeEnvelopeResourceSource private lateinit var logSource: FakeLogPayloadSource + private lateinit var cachedLogEnvelopeStore: FakeCachedLogEnvelopeStore private lateinit var logEnvelopeSource: LogEnvelopeSourceImpl @Before fun setup() { metadataSource = FakeEnvelopeMetadataSource() resourceSource = FakeEnvelopeResourceSource() - logSource = FakeLogPayloadSource() + logSource = FakeLogPayloadSource().apply { + batchedLogPayloadSource = fakeBatchedPayload + } + cachedLogEnvelopeStore = FakeCachedLogEnvelopeStore() logEnvelopeSource = LogEnvelopeSourceImpl( metadataSource, resourceSource, logSource, + cachedLogEnvelopeStore, ) } @Test fun getBatchedLogEnvelope() { - logSource.singleLogPayloadsSource = mutableListOf>().apply { - repeat(5) { - add(LogRequest(LogPayload(listOf(sendImmediatelyLog.copy(body = "$it"))))) - } - } with(logEnvelopeSource.getBatchedLogEnvelope()) { assertEquals(metadataSource.metadata, metadata) assertEquals(resourceSource.resource, resource) - assertEquals(logSource.getBatchedLogPayload(), data) + assertEquals(fakeBatchedPayload, data) assertEquals("logs", type) assertEquals("0.1.0", version) } @@ -48,10 +61,22 @@ internal class LogEnvelopeSourceImplTest { @Test fun getSingleLogEnvelopes() { - with(logEnvelopeSource.getSingleLogEnvelopes().single().payload) { + logSource.singleLogPayloadsSource = mutableListOf>().apply { + repeat(5) { + add(LogRequest(LogPayload(listOf(sendImmediatelyLog.copy(body = "$it"))))) + } + } + with(logEnvelopeSource.getSingleLogEnvelopes().first().payload) { + assertEquals(metadataSource.metadata, metadata) + assertEquals(resourceSource.resource, resource) + assertEquals(sendImmediatelyLog.copy(body = "0"), data.logs?.single()) + assertEquals("logs", type) + assertEquals("0.1.0", version) + } + with(logEnvelopeSource.getSingleLogEnvelopes().last().payload) { assertEquals(metadataSource.metadata, metadata) assertEquals(resourceSource.resource, resource) - assertEquals(logSource.getSingleLogPayloads().single().payload, data) + assertEquals(sendImmediatelyLog.copy(body = "4"), data.logs?.single()) assertEquals("logs", type) assertEquals("0.1.0", version) } @@ -67,4 +92,62 @@ internal class LogEnvelopeSourceImplTest { assertEquals("0.1.0", version) } } + + @Test + fun `check native crash envelope`() { + val crashPayload = LogPayload(logs = listOf(nativeCrashLog)) + val crashLogAttributes = checkNotNull(nativeCrashLog.attributes) + val expectedSessionId = crashLogAttributes.findAttributeValue(SessionIncubatingAttributes.SESSION_ID.key) + val expectedProcessIdentifier = crashLogAttributes.findAttributeValue(embProcessIdentifier.name) + val cachedCrashEnvelopeMetadata = createNativeCrashEnvelopeMetadata( + sessionId = expectedSessionId, + processIdentifier = expectedProcessIdentifier, + ) + logSource.singleLogPayloadsSource = listOf( + LogRequest(crashPayload) + ) + cachedLogEnvelopeStore.create( + storedTelemetryMetadata = cachedCrashEnvelopeMetadata, + resource = fakeEnvelopeResource, + metadata = fakeEnvelopeMetadata + ) + with(logEnvelopeSource.getSingleLogEnvelopes().first().payload) { + assertEquals(fakeEnvelopeResource, resource) + assertEquals(fakeEnvelopeMetadata, metadata) + assertEquals(nativeCrashLog, data.logs?.single()) + } + + with(cachedLogEnvelopeStore.envelopeGetRequest.single()) { + assertEquals(0L, timestamp) + assertEquals(expectedSessionId, uuid) + assertEquals(expectedProcessIdentifier, processId) + assertEquals(SupportedEnvelopeType.CRASH, envelopeType) + assertEquals(PayloadType.NATIVE_CRASH, payloadType) + } + } + + @Test + fun `check native crash envelope when no matching session found`() { + logSource.singleLogPayloadsSource = listOf( + LogRequest(LogPayload(logs = listOf(nativeCrashWithoutSessionLog))) + ) + cachedLogEnvelopeStore.create( + storedTelemetryMetadata = createNativeCrashEnvelopeMetadata(), + resource = fakeEnvelopeResource, + metadata = fakeEnvelopeMetadata + ) + with(logEnvelopeSource.getSingleLogEnvelopes().first().payload) { + assertEquals(fakeEnvelopeResource, resource) + assertEquals(fakeEnvelopeMetadata, metadata) + assertEquals(nativeCrashWithoutSessionLog, data.logs?.single()) + } + + with(cachedLogEnvelopeStore.envelopeGetRequest.single()) { + assertEquals(0L, timestamp) + assertEquals("none", uuid) + assertEquals("none", processId) + assertEquals(SupportedEnvelopeType.CRASH, envelopeType) + assertEquals(PayloadType.NATIVE_CRASH, payloadType) + } + } } diff --git a/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/injection/DeliveryModuleImplTest.kt b/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/injection/DeliveryModuleImplTest.kt index 2f69cb6e37..e7cca9ca9e 100644 --- a/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/injection/DeliveryModuleImplTest.kt +++ b/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/injection/DeliveryModuleImplTest.kt @@ -55,6 +55,7 @@ class DeliveryModuleImplTest { assertNotNull(module.payloadCachingService) assertNotNull(module.payloadStorageService) assertNotNull(module.cacheStorageService) + assertNotNull(module.cachedLogEnvelopeStore) assertNotNull(module.requestExecutionService) assertNotNull(module.schedulingService) assertTrue(module.payloadStore is V2PayloadStore) @@ -69,6 +70,7 @@ class DeliveryModuleImplTest { assertNull(module.payloadCachingService) assertNull(module.payloadStorageService) assertNull(module.cacheStorageService) + assertNull(module.cachedLogEnvelopeStore) assertTrue(module.requestExecutionService is FakeRequestExecutionService) assertNull(module.schedulingService) assertNull(module.payloadStore) diff --git a/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/resurrection/PayloadResurrectionServiceImplTest.kt b/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/resurrection/PayloadResurrectionServiceImplTest.kt index 5bdd472863..5dba274395 100644 --- a/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/resurrection/PayloadResurrectionServiceImplTest.kt +++ b/embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/resurrection/PayloadResurrectionServiceImplTest.kt @@ -6,6 +6,7 @@ import io.embrace.android.embracesdk.assertions.findSpansByName import io.embrace.android.embracesdk.assertions.getLastHeartbeatTimeMs import io.embrace.android.embracesdk.assertions.getSessionId import io.embrace.android.embracesdk.assertions.getStartTime +import io.embrace.android.embracesdk.fakes.FakeCachedLogEnvelopeStore import io.embrace.android.embracesdk.fakes.FakeEmbLogger import io.embrace.android.embracesdk.fakes.FakeIntakeService import io.embrace.android.embracesdk.fakes.FakeNativeCrashService @@ -14,14 +15,19 @@ import io.embrace.android.embracesdk.fakes.FakePersistableEmbraceSpan import io.embrace.android.embracesdk.fakes.FakeSpanData.Companion.perfSpanSnapshot import io.embrace.android.embracesdk.fakes.TestPlatformSerializer import io.embrace.android.embracesdk.fakes.fakeEmptyLogEnvelope +import io.embrace.android.embracesdk.fakes.fakeEnvelopeMetadata +import io.embrace.android.embracesdk.fakes.fakeEnvelopeResource import io.embrace.android.embracesdk.fakes.fakeIncompleteSessionEnvelope +import io.embrace.android.embracesdk.fakes.fakeLaterEnvelopeMetadata +import io.embrace.android.embracesdk.fakes.fakeLaterEnvelopeResource 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.PayloadType import io.embrace.android.embracesdk.internal.delivery.StoredTelemetryMetadata import io.embrace.android.embracesdk.internal.delivery.SupportedEnvelopeType +import io.embrace.android.embracesdk.internal.delivery.SupportedEnvelopeType.CRASH import io.embrace.android.embracesdk.internal.opentelemetry.embCrashId import io.embrace.android.embracesdk.internal.opentelemetry.embState import io.embrace.android.embracesdk.internal.payload.Envelope @@ -34,9 +40,9 @@ import io.embrace.android.embracesdk.internal.spans.findAttributeValue import io.embrace.android.embracesdk.internal.spans.toEmbraceSpanData import io.embrace.android.embracesdk.spans.ErrorCode import io.opentelemetry.api.trace.SpanId -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertTrue +import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -44,6 +50,7 @@ class PayloadResurrectionServiceImplTest { private lateinit var intakeService: FakeIntakeService private lateinit var cacheStorageService: FakePayloadStorageService + private lateinit var cachedLogEnvelopeStore: FakeCachedLogEnvelopeStore private lateinit var nativeCrashService: FakeNativeCrashService private lateinit var logger: FakeEmbLogger private lateinit var serializer: TestPlatformSerializer @@ -53,12 +60,14 @@ class PayloadResurrectionServiceImplTest { fun setUp() { intakeService = FakeIntakeService() cacheStorageService = FakePayloadStorageService() + cachedLogEnvelopeStore = FakeCachedLogEnvelopeStore() nativeCrashService = FakeNativeCrashService() logger = FakeEmbLogger(false) serializer = TestPlatformSerializer() resurrectionService = PayloadResurrectionServiceImpl( intakeService = intakeService, cacheStorageService = cacheStorageService, + cachedLogEnvelopeStore = cachedLogEnvelopeStore, logger = logger, serializer = serializer, ) @@ -66,7 +75,7 @@ class PayloadResurrectionServiceImplTest { @Test fun `if no previous cached session then send previous cached sessions should not send anything`() { - resurrectionService.resurrectOldPayloads({ nativeCrashService }) + resurrectionService.resurrectOldPayloads { nativeCrashService } assertTrue(intakeService.intakeList.isEmpty()) } @@ -96,12 +105,12 @@ class PayloadResurrectionServiceImplTest { @Test fun `all payloads from previous app launches are deleted after resurrection`() { cacheStorageService.addPayload( - metadata = fakeNativeCrashStoredTelemetryMetadata, + metadata = fakeCachedCrashEnvelopeMetadata, data = fakeEmptyLogEnvelope() ) assertEquals(1, cacheStorageService.storedPayloadCount()) assertEquals(0, cacheStorageService.deleteCount.get()) - resurrectionService.resurrectOldPayloads({ nativeCrashService }) + resurrectionService.resurrectOldPayloads { nativeCrashService } assertTrue(intakeService.getIntakes().isEmpty()) assertEquals(1, cacheStorageService.deleteCount.get()) @@ -184,7 +193,7 @@ class PayloadResurrectionServiceImplTest { data = deadSessionEnvelope ) serializer.errorOnNextOperation() - resurrectionService.resurrectOldPayloads({ nativeCrashService }) + resurrectionService.resurrectOldPayloads { nativeCrashService } assertResurrectionFailure() } @@ -206,11 +215,15 @@ class PayloadResurrectionServiceImplTest { data = deadSessionEnvelope ) + val oldResource = fakeEnvelopeResource.copy(appVersion = "1.4", sdkVersion = "6.13", osVersion = "10") + val oldMetadata = fakeEnvelopeMetadata.copy(username = "old-admin") val earlierDeadSession = fakeIncompleteSessionEnvelope( sessionId = "anotherFakeSessionId", startMs = deadSessionEnvelope.getStartTime() - 100_000L, lastHeartbeatTimeMs = deadSessionEnvelope.getStartTime() - 90_000L, - sessionProperties = mapOf("prop" to "earlier") + sessionProperties = mapOf("prop" to "earlier"), + resource = oldResource, + metadata = oldMetadata ) val earlierSessionCrashData = createNativeCrashData( @@ -230,7 +243,7 @@ class PayloadResurrectionServiceImplTest { data = earlierDeadSession ) - resurrectionService.resurrectOldPayloads({ nativeCrashService }) + resurrectionService.resurrectOldPayloads { nativeCrashService } val sessionPayloads = intakeService.getIntakes() assertEquals(2, sessionPayloads.size) @@ -260,6 +273,17 @@ class PayloadResurrectionServiceImplTest { ) } + val createdEnvelopes = cachedLogEnvelopeStore.createdEnvelopes + assertEquals(2, createdEnvelopes.size) + with(createdEnvelopes.first()) { + assertEquals(fakeEnvelopeResource, resource) + assertEquals(fakeEnvelopeMetadata, metadata) + } + with(createdEnvelopes.last()) { + assertEquals(oldResource, resource) + assertEquals(oldMetadata, metadata) + } + assertEquals(2, nativeCrashService.nativeCrashesSent.size) with(nativeCrashService.nativeCrashesSent.first()) { assertEquals(deadSessionCrashData, first) @@ -276,14 +300,46 @@ class PayloadResurrectionServiceImplTest { @Test fun `native crashes without sessions are sent properly`() { + val deadSessionCrashData = createNativeCrashData( + nativeCrashId = "native-crash-1", + sessionId = "no-session-id" + ) + cacheStorageService.addPayload( + metadata = fakeCachedCrashEnvelopeMetadata, + data = fakeEmptyLogEnvelope( + resource = fakeLaterEnvelopeResource, + metadata = fakeLaterEnvelopeMetadata + ) + ) + nativeCrashService.addNativeCrashData(deadSessionCrashData) + resurrectionService.resurrectOldPayloads { nativeCrashService } + + assertEquals(0, intakeService.getIntakes().size) + + with(cachedLogEnvelopeStore.createdEnvelopes.single()) { + assertEquals(fakeLaterEnvelopeResource, resource) + assertEquals(fakeLaterEnvelopeMetadata, metadata) + } + + assertEquals(1, nativeCrashService.nativeCrashesSent.size) + with(nativeCrashService.nativeCrashesSent.first()) { + assertEquals(deadSessionCrashData, first) + assertTrue(second.keys.none { it.isSessionPropertyAttributeName() || embState.name == it }) + } + } + + @Test + fun `native crashes without sessions or cached crash envelopes sent`() { val deadSessionCrashData = createNativeCrashData( nativeCrashId = "native-crash-1", sessionId = "no-session-id" ) nativeCrashService.addNativeCrashData(deadSessionCrashData) - resurrectionService.resurrectOldPayloads({ nativeCrashService }) + resurrectionService.resurrectOldPayloads { nativeCrashService } assertEquals(0, intakeService.getIntakes().size) + + assertTrue(cachedLogEnvelopeStore.createdEnvelopes.isEmpty()) assertEquals(1, nativeCrashService.nativeCrashesSent.size) with(nativeCrashService.nativeCrashesSent.first()) { assertEquals(deadSessionCrashData, first) @@ -296,7 +352,7 @@ class PayloadResurrectionServiceImplTest { metadata = sessionMetadata, data = this ) - resurrectionService.resurrectOldPayloads({ nativeCrashService }) + resurrectionService.resurrectOldPayloads { nativeCrashService } } private fun createNativeCrashData( @@ -351,5 +407,13 @@ class PayloadResurrectionServiceImplTest { ) ) ) + val fakeCachedCrashEnvelopeMetadata = StoredTelemetryMetadata( + timestamp = 1000L, + uuid = "old-session-id", + processId = "old-process-id", + envelopeType = CRASH, + complete = false, + payloadType = PayloadType.UNKNOWN + ) } } diff --git a/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/PayloadType.kt b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/PayloadType.kt index 97f40c0a18..063327e632 100644 --- a/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/PayloadType.kt +++ b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/PayloadType.kt @@ -12,6 +12,7 @@ enum class PayloadType( AEI("sys.exit"), EXCEPTION("sys.exception"), NETWORK_CAPTURE("sys.network_capture"), + INTERNAL_ERROR("sys.internal"), UNKNOWN("unknown"); companion object { @@ -30,6 +31,7 @@ enum class PayloadType( AEI -> "aei" EXCEPTION -> "exception" NETWORK_CAPTURE -> "network" + INTERNAL_ERROR -> "internal" else -> "unknown" } } @@ -45,6 +47,7 @@ enum class PayloadType( "aei" -> AEI "exception" -> EXCEPTION "network" -> NETWORK_CAPTURE + "internal" -> INTERNAL_ERROR else -> UNKNOWN } } diff --git a/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/StoredTelemetryMetadata.kt b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/StoredTelemetryMetadata.kt index 339d057309..a1e22cf844 100644 --- a/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/StoredTelemetryMetadata.kt +++ b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/StoredTelemetryMetadata.kt @@ -15,12 +15,12 @@ data class StoredTelemetryMetadata( val envelopeType: SupportedEnvelopeType, val complete: Boolean = true, val payloadType: PayloadType = PayloadType.UNKNOWN, +) { val filename: String = "${envelopeType.priority}_${timestamp}_${uuid}_${processId}_${complete}_${ toFilenamePart( payloadType ) - }_v1.json", -) { + }_v1.json" companion object { /** @@ -52,7 +52,6 @@ data class StoredTelemetryMetadata( envelopeType, complete, payloadType, - filename ) ) } diff --git a/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/CachedLogEnvelopeStore.kt b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/CachedLogEnvelopeStore.kt new file mode 100644 index 0000000000..267cf0f270 --- /dev/null +++ b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/CachedLogEnvelopeStore.kt @@ -0,0 +1,50 @@ +package io.embrace.android.embracesdk.internal.delivery.storage + +import io.embrace.android.embracesdk.internal.delivery.PayloadType +import io.embrace.android.embracesdk.internal.delivery.StoredTelemetryMetadata +import io.embrace.android.embracesdk.internal.delivery.SupportedEnvelopeType +import io.embrace.android.embracesdk.internal.payload.Envelope +import io.embrace.android.embracesdk.internal.payload.EnvelopeMetadata +import io.embrace.android.embracesdk.internal.payload.EnvelopeResource +import io.embrace.android.embracesdk.internal.payload.LogPayload + +/** + * Create and make available [Envelope] objects with custom [EnvelopeResource] and [EnvelopeMetadata] that differ from + * the values used by the current SDK instance so telemetry from past app launches can be created and sent. + */ +interface CachedLogEnvelopeStore { + + /** + * Create the envelope to be used for the given [StoredTelemetryMetadata] using the given + * [EnvelopeResource] and [EnvelopeMetadata] + */ + fun create( + storedTelemetryMetadata: StoredTelemetryMetadata, + resource: EnvelopeResource, + metadata: EnvelopeMetadata, + ) + + /** + * Return the envelope to be used for the given [StoredTelemetryMetadata] if it exists + */ + fun get(storedTelemetryMetadata: StoredTelemetryMetadata): Envelope? + + /** + * Delete all cached envelopes. This should be called when all pending log payloads from previous SDK instances + * have been created + */ + fun clear() + + companion object { + fun createNativeCrashEnvelopeMetadata( + sessionId: String? = null, + processIdentifier: String? = null, + ) = StoredTelemetryMetadata( + timestamp = 0L, + uuid = sessionId ?: "none", + processId = processIdentifier ?: "none", + envelopeType = SupportedEnvelopeType.CRASH, + payloadType = PayloadType.NATIVE_CRASH, + ) + } +} diff --git a/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/CachedLogEnvelopeStoreImpl.kt b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/CachedLogEnvelopeStoreImpl.kt new file mode 100644 index 0000000000..a21795d6b5 --- /dev/null +++ b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/CachedLogEnvelopeStoreImpl.kt @@ -0,0 +1,61 @@ +package io.embrace.android.embracesdk.internal.delivery.storage + +import io.embrace.android.embracesdk.internal.delivery.StoredTelemetryMetadata +import io.embrace.android.embracesdk.internal.logging.EmbLogger +import io.embrace.android.embracesdk.internal.payload.Envelope +import io.embrace.android.embracesdk.internal.payload.Envelope.Companion.createLogEnvelope +import io.embrace.android.embracesdk.internal.payload.EnvelopeMetadata +import io.embrace.android.embracesdk.internal.payload.EnvelopeResource +import io.embrace.android.embracesdk.internal.payload.LogPayload +import io.embrace.android.embracesdk.internal.serialization.PlatformSerializer +import io.embrace.android.embracesdk.internal.worker.PriorityWorker +import java.io.File + +class CachedLogEnvelopeStoreImpl( + outputDir: Lazy, + worker: PriorityWorker, + logger: EmbLogger, + private val serializer: PlatformSerializer, + storageLimit: Int = 100, +) : CachedLogEnvelopeStore { + + private val fileStorageService: FileStorageService = FileStorageServiceImpl( + outputDir, + worker, + logger, + storageLimit + ) + + override fun create( + storedTelemetryMetadata: StoredTelemetryMetadata, + resource: EnvelopeResource, + metadata: EnvelopeMetadata, + ) { + fileStorageService.store(storedTelemetryMetadata) { stream -> + serializer.toJson( + LogPayload().createLogEnvelope( + resource, + metadata + ), + storedTelemetryMetadata.envelopeType.serializedType, + stream + ) + } + } + + override fun get(storedTelemetryMetadata: StoredTelemetryMetadata): Envelope? = + runCatching { + fileStorageService.loadPayloadAsStream(storedTelemetryMetadata)?.let { inputStream -> + serializer.fromJson>( + inputStream = inputStream, + type = storedTelemetryMetadata.envelopeType.serializedType + ) + } + }.getOrNull() + + override fun clear() { + fileStorageService.getStoredPayloads().forEach { + fileStorageService.delete(it) + } + } +} diff --git a/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/StorageLocation.kt b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/StorageLocation.kt index 88ccff57a2..276bbd12b8 100644 --- a/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/StorageLocation.kt +++ b/embrace-android-delivery/src/main/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/StorageLocation.kt @@ -20,7 +20,12 @@ enum class StorageLocation(private val dir: String) { /** * Native Embrace crash reports */ - NATIVE("embrace_native"); + NATIVE("embrace_native"), + + /** + * Cached envelopes + */ + ENVELOPE("embrace_envelopes"); /** * Get the directory as a [File] object diff --git a/embrace-android-delivery/src/test/kotlin/io/embrace/android/embracesdk/internal/delivery/intake/IntakeServiceImplTest.kt b/embrace-android-delivery/src/test/kotlin/io/embrace/android/embracesdk/internal/delivery/intake/IntakeServiceImplTest.kt index 8e2227f21d..2a48669b53 100644 --- a/embrace-android-delivery/src/test/kotlin/io/embrace/android/embracesdk/internal/delivery/intake/IntakeServiceImplTest.kt +++ b/embrace-android-delivery/src/test/kotlin/io/embrace/android/embracesdk/internal/delivery/intake/IntakeServiceImplTest.kt @@ -297,7 +297,7 @@ class IntakeServiceImplTest { fun `new empty crash envelope caching will remove the old copy`() { executorService.blockingMode = false val cache1 = - StoredTelemetryMetadata(clock.now(), UUID, PROCESS_ID, CRASH, false, PayloadType.NATIVE_CRASH).apply { + StoredTelemetryMetadata(clock.now(), UUID, PROCESS_ID, CRASH, false, PayloadType.UNKNOWN).apply { intakeService.take( intake = logEnvelope, metadata = this @@ -307,7 +307,7 @@ class IntakeServiceImplTest { assertEquals(cache1.filename, cacheStorageService.storedFilenames().single()) val cache2 = - StoredTelemetryMetadata(clock.tick(), UUID, PROCESS_ID, CRASH, false, PayloadType.NATIVE_CRASH).apply { + StoredTelemetryMetadata(clock.tick(), UUID, PROCESS_ID, CRASH, false, PayloadType.UNKNOWN).apply { intakeService.take( intake = logEnvelope, metadata = this diff --git a/embrace-android-delivery/src/test/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/CachedLogEnvelopeStoreImplTest.kt b/embrace-android-delivery/src/test/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/CachedLogEnvelopeStoreImplTest.kt new file mode 100644 index 0000000000..c427b17a56 --- /dev/null +++ b/embrace-android-delivery/src/test/kotlin/io/embrace/android/embracesdk/internal/delivery/storage/CachedLogEnvelopeStoreImplTest.kt @@ -0,0 +1,74 @@ +package io.embrace.android.embracesdk.internal.delivery.storage + +import io.embrace.android.embracesdk.concurrency.BlockingScheduledExecutorService +import io.embrace.android.embracesdk.fakes.FakeEmbLogger +import io.embrace.android.embracesdk.fakes.TestPlatformSerializer +import io.embrace.android.embracesdk.fakes.fakeEnvelopeMetadata +import io.embrace.android.embracesdk.fakes.fakeEnvelopeResource +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore.Companion.createNativeCrashEnvelopeMetadata +import io.embrace.android.embracesdk.internal.serialization.PlatformSerializer +import io.embrace.android.embracesdk.internal.worker.PriorityWorker +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import java.io.File +import java.nio.file.Files + +class CachedLogEnvelopeStoreImplTest { + + private lateinit var outputDir: File + private lateinit var store: CachedLogEnvelopeStoreImpl + private lateinit var logger: FakeEmbLogger + private lateinit var serializer: PlatformSerializer + private lateinit var executor: BlockingScheduledExecutorService + + @Before + fun setUp() { + outputDir = Files.createTempDirectory("temp").toFile().apply { + mkdirs() + } + logger = FakeEmbLogger() + serializer = TestPlatformSerializer() + executor = BlockingScheduledExecutorService() + + store = CachedLogEnvelopeStoreImpl( + outputDir = lazy { outputDir }, + worker = PriorityWorker(executor), + logger = logger, + serializer = serializer + ) + } + + @Test + fun `payload can be written, retrieved, and deleted`() { + store.create( + storedTelemetryMetadata = fakeNativeCrashEnvelope, + resource = fakeEnvelopeResource, + metadata = fakeEnvelopeMetadata + ) + + val anotherCrashMetadata = createNativeCrashEnvelopeMetadata( + sessionId = "another-session", + processIdentifier = "another-process" + ) + + assertNull(store.get(anotherCrashMetadata)) + val envelope = checkNotNull(store.get(fakeNativeCrashEnvelope)) + with(envelope) { + assertEquals(fakeEnvelopeResource, resource) + assertEquals(fakeEnvelopeMetadata, metadata) + } + + store.clear() + executor.queueCompletionTask() + assertNull(store.get(fakeNativeCrashEnvelope)) + } + + companion object { + val fakeNativeCrashEnvelope = createNativeCrashEnvelopeMetadata( + sessionId = "old-session-id", + processIdentifier = "old-process-id" + ) + } +} diff --git a/embrace-android-infra/src/main/kotlin/io/embrace/android/embracesdk/internal/logging/InternalErrorType.kt b/embrace-android-infra/src/main/kotlin/io/embrace/android/embracesdk/internal/logging/InternalErrorType.kt index 53c3493721..ed755e622b 100644 --- a/embrace-android-infra/src/main/kotlin/io/embrace/android/embracesdk/internal/logging/InternalErrorType.kt +++ b/embrace-android-infra/src/main/kotlin/io/embrace/android/embracesdk/internal/logging/InternalErrorType.kt @@ -22,6 +22,7 @@ enum class InternalErrorType { WEB_VITAL_PARSE_FAIL, NATIVE_THREAD_SAMPLE_FAIL, NATIVE_CRASH_LOAD_FAIL, + NATIVE_CRASH_RESURRECTION_ERROR, INVALID_NATIVE_SYMBOLS, NATIVE_HANDLER_INSTALL_FAIL, SAFE_DATA_CAPTURE_FAIL, diff --git a/embrace-android-payload/src/main/kotlin/io/embrace/android/embracesdk/internal/payload/Envelope.kt b/embrace-android-payload/src/main/kotlin/io/embrace/android/embracesdk/internal/payload/Envelope.kt index fe07474fb4..20ec97c4e7 100644 --- a/embrace-android-payload/src/main/kotlin/io/embrace/android/embracesdk/internal/payload/Envelope.kt +++ b/embrace-android-payload/src/main/kotlin/io/embrace/android/embracesdk/internal/payload/Envelope.kt @@ -36,5 +36,14 @@ data class Envelope( Types.newParameterizedType(Envelope::class.java, SessionPayload::class.java) val logEnvelopeType: ParameterizedType = Types.newParameterizedType(Envelope::class.java, LogPayload::class.java) + + fun LogPayload.createLogEnvelope(resource: EnvelopeResource, metadata: EnvelopeMetadata) = + Envelope( + resource = resource, + metadata = metadata, + version = "0.1.0", + type = "logs", + data = this + ) } } diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/features/NativeCrashFeatureTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/features/NativeCrashFeatureTest.kt index b2bda20d45..962302ca48 100644 --- a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/features/NativeCrashFeatureTest.kt +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/features/NativeCrashFeatureTest.kt @@ -7,6 +7,10 @@ import io.embrace.android.embracesdk.fakes.FakePayloadStorageService import io.embrace.android.embracesdk.fakes.TestPlatformSerializer import io.embrace.android.embracesdk.fakes.config.FakeEnabledFeatureConfig import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig +import io.embrace.android.embracesdk.fakes.fakeEnvelopeMetadata +import io.embrace.android.embracesdk.fakes.fakeEnvelopeResource +import io.embrace.android.embracesdk.fakes.fakeLaterEnvelopeMetadata +import io.embrace.android.embracesdk.fakes.fakeLaterEnvelopeResource import io.embrace.android.embracesdk.internal.delivery.PayloadType import io.embrace.android.embracesdk.internal.delivery.StoredTelemetryMetadata import io.embrace.android.embracesdk.internal.delivery.SupportedEnvelopeType @@ -62,29 +66,33 @@ internal class NativeCrashFeatureTest { ) private val crashMetadata = StoredTelemetryMetadata( timestamp = sessionMetadata.timestamp + 1_000L, - uuid = "94c0e427-9faf-4dac-b1d5-2fd74039ef2d", - processId = "f0652f96-8e76-4f68-8545-14f6229f01f8", + uuid = "EB96C6A8AF09449A8547C7703CE6BDAE", + processId = "8115ec91-3e5e-4d8a-816d-cc40306f9822", envelopeType = SupportedEnvelopeType.CRASH, + complete = false, payloadType = PayloadType.NATIVE_CRASH, ) private val crashMetadata2 = StoredTelemetryMetadata( timestamp = sessionMetadata2.timestamp + 1_000L, - uuid = "bb690ad1-6b87-4e08-b72c-7deca14451d8", - processId = "bb15ec91-3e5e-4d8a-816d-cc40306f9822", + uuid = "A0A0C6A8AF09449A8547C7703CE6BDAE", + processId = "aa15ec91-3e5e-4d8a-816d-cc40306f9822", envelopeType = SupportedEnvelopeType.CRASH, + complete = false, payloadType = PayloadType.NATIVE_CRASH, ) private val crashData = createStoredNativeCrashData( - serializer, - sessionMetadata, - crashMetadata, - "native_crash_1.txt", + serializer = serializer, + resourceFixtureName = "native_crash_1.txt", + crashMetadata = crashMetadata, + sessionMetadata = sessionMetadata, ) private val crashData2 = createStoredNativeCrashData( - serializer, - sessionMetadata2, - crashMetadata2, - "native_crash_2.txt", + serializer = serializer, + resourceFixtureName = "native_crash_2.txt", + crashMetadata = crashMetadata2, + sessionMetadata = sessionMetadata2, + envelopeResource = fakeLaterEnvelopeResource, + envelopeMetadata = fakeLaterEnvelopeMetadata ) private lateinit var cacheStorageService: FakePayloadStorageService @@ -92,7 +100,9 @@ internal class NativeCrashFeatureTest { @Rule @JvmField val testRule: IntegrationTestRule = IntegrationTestRule { - EmbraceSetupInterface().apply { + EmbraceSetupInterface( + processIdentifier = "8115ec91-3e5e-4d8a-816d-cc40306f9822" + ).apply { (overriddenInitModule.logger as FakeEmbLogger).throwOnInternalError = false } } @@ -107,7 +117,10 @@ internal class NativeCrashFeatureTest { testRule.runTest( instrumentedConfig = config, setupAction = { - setupFakeDeadSession(cacheStorageService, crashData) + setupCachedDataFromNativeCrash( + storageService = cacheStorageService, + crashData = crashData + ) setupFakeNativeCrash(serializer, crashData) }, testCaseAction = {}, @@ -116,6 +129,10 @@ internal class NativeCrashFeatureTest { assertDeadSessionResurrected(crashData) } val envelope = getSingleLogEnvelope() + with(envelope) { + assertEquals(fakeEnvelopeResource, resource) + assertEquals(fakeEnvelopeMetadata, metadata) + } val log = envelope.getLastLog() assertNativeCrashSent(log, crashData, testRule.setup.symbols) } @@ -127,12 +144,22 @@ internal class NativeCrashFeatureTest { testRule.runTest( instrumentedConfig = config, setupAction = { - setupFakeNativeCrash(serializer, crashData) + val modifiedCrashData = crashData.copy(sessionMetadata = null, sessionEnvelope = null) + setupCachedDataFromNativeCrash( + storageService = cacheStorageService, + crashData = modifiedCrashData + ) + setupFakeNativeCrash(serializer, modifiedCrashData) + }, + testCaseAction = { + recordSession() }, - testCaseAction = {}, assertAction = { - assertEquals(0, getSessionEnvelopes(0).size) val envelope = getSingleLogEnvelope() + with(envelope) { + assertEquals(fakeEnvelopeResource, resource) + assertEquals(fakeEnvelopeMetadata, metadata) + } val log = envelope.getLastLog() assertNativeCrashSent(log, crashData, testRule.setup.symbols) } @@ -144,7 +171,7 @@ internal class NativeCrashFeatureTest { testRule.runTest( instrumentedConfig = config, setupAction = { - setupFakeDeadSession(cacheStorageService, crashData) + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData) }, testCaseAction = {}, assertAction = { @@ -161,8 +188,8 @@ internal class NativeCrashFeatureTest { testRule.runTest( instrumentedConfig = config, setupAction = { - setupFakeDeadSession(cacheStorageService, crashData) - setupFakeDeadSession(cacheStorageService, crashData2) + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData) + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData2) setupFakeNativeCrash(serializer, crashData) setupFakeNativeCrash(serializer, crashData2) }, @@ -170,10 +197,24 @@ internal class NativeCrashFeatureTest { assertAction = { val sessionEnvelopes = getSessionEnvelopes(2) val logEnvelopes = getLogEnvelopes(2) + val expectedCrashEnvelope = checkNotNull(crashData.cachedCrashEnvelope) // crashes sent - val log1 = logEnvelopes.single { findMatchingSessionId(it, crashData) }.getLastLog() - val log2 = logEnvelopes.single { findMatchingSessionId(it, crashData2) }.getLastLog() + val crashEnvelope1 = logEnvelopes.single { findMatchingSessionId(it, crashData) } + with(crashEnvelope1) { + assertEquals(expectedCrashEnvelope.resource, resource) + assertEquals(expectedCrashEnvelope.metadata, metadata) + } + + val crashEnvelope2 = logEnvelopes.single { findMatchingSessionId(it, crashData2) } + with(crashEnvelope2) { + assertEquals(fakeLaterEnvelopeResource, resource) + assertEquals(fakeLaterEnvelopeMetadata, metadata) + } + + val log1 = crashEnvelope1.getLastLog() + val log2 = crashEnvelope2.getLastLog() + assertNativeCrashSent(log1, crashData, testRule.setup.symbols) assertNativeCrashSent(log2, crashData2, testRule.setup.symbols) @@ -193,7 +234,7 @@ internal class NativeCrashFeatureTest { testRule.runTest( instrumentedConfig = FakeInstrumentedConfig(), setupAction = { - setupFakeDeadSession(cacheStorageService, crashData) + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData) setupFakeNativeCrash(serializer, crashData) }, testCaseAction = {}, @@ -212,7 +253,7 @@ internal class NativeCrashFeatureTest { testRule.runTest( instrumentedConfig = config, setupAction = { - setupFakeDeadSession(cacheStorageService, crashData) + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData) setupFakeNativeCrash(serializer, crashData) // simulate JNI call failing to load struct @@ -233,7 +274,7 @@ internal class NativeCrashFeatureTest { testRule.runTest( instrumentedConfig = config, setupAction = { - setupFakeDeadSession(cacheStorageService, crashData) + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData) setupFakeNativeCrash(serializer, crashData) // simulate bad JSON diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/features/ResurrectionFeatureTest.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/features/ResurrectionFeatureTest.kt index 1d22b59d53..ca16c93f08 100644 --- a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/features/ResurrectionFeatureTest.kt +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/features/ResurrectionFeatureTest.kt @@ -1,10 +1,13 @@ package io.embrace.android.embracesdk.testcases.features import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.embrace.android.embracesdk.fakes.FakeEmbLogger import io.embrace.android.embracesdk.fakes.FakePayloadStorageService import io.embrace.android.embracesdk.fakes.TestPlatformSerializer import io.embrace.android.embracesdk.fakes.config.FakeEnabledFeatureConfig import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig +import io.embrace.android.embracesdk.fakes.fakeEnvelopeMetadata +import io.embrace.android.embracesdk.fakes.fakeEnvelopeResource import io.embrace.android.embracesdk.fixtures.fakeCachedSessionStoredTelemetryMetadata import io.embrace.android.embracesdk.fixtures.fakeNativeCrashStoredTelemetryMetadata import io.embrace.android.embracesdk.internal.config.remote.BackgroundActivityRemoteConfig @@ -13,9 +16,12 @@ import io.embrace.android.embracesdk.internal.config.remote.RemoteConfig import io.embrace.android.embracesdk.internal.delivery.PayloadType import io.embrace.android.embracesdk.internal.delivery.StoredTelemetryMetadata import io.embrace.android.embracesdk.internal.delivery.SupportedEnvelopeType +import io.embrace.android.embracesdk.internal.spans.findAttributeValue import io.embrace.android.embracesdk.testframework.IntegrationTestRule +import io.embrace.android.embracesdk.testframework.actions.EmbraceSetupInterface import io.embrace.android.embracesdk.testframework.actions.createStoredNativeCrashData import io.embrace.android.embracesdk.testframework.assertions.getLastLog +import io.opentelemetry.semconv.ExceptionAttributes import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -33,7 +39,11 @@ internal class ResurrectionFeatureTest { @Rule @JvmField - val testRule: IntegrationTestRule = IntegrationTestRule() + val testRule: IntegrationTestRule = IntegrationTestRule { + EmbraceSetupInterface().apply { + (overriddenInitModule.logger as FakeEmbLogger).throwOnInternalError = false + } + } @Before fun setUp() { @@ -48,15 +58,15 @@ internal class ResurrectionFeatureTest { @Test fun `crashed session and native crash resurrected and sent properly`() { val crashData = createStoredNativeCrashData( - serializer, - fakeCachedSessionStoredTelemetryMetadata, - fakeNativeCrashStoredTelemetryMetadata, - "native_crash_1.txt", + serializer = serializer, + resourceFixtureName = "native_crash_1.txt", + crashMetadata = fakeNativeCrashStoredTelemetryMetadata, + sessionMetadata = fakeCachedSessionStoredTelemetryMetadata, ) testRule.runTest( instrumentedConfig = FakeInstrumentedConfig(enabledFeatures = FakeEnabledFeatureConfig(nativeCrashCapture = true)), setupAction = { - setupFakeDeadSession(cacheStorageService, crashData) + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData) setupFakeNativeCrash(serializer, crashData) }, testCaseAction = {}, @@ -65,12 +75,121 @@ internal class ResurrectionFeatureTest { assertDeadSessionResurrected(crashData) } val envelope = getSingleLogEnvelope() + with(envelope) { + assertEquals(fakeEnvelopeResource, resource) + assertEquals(fakeEnvelopeMetadata, metadata) + } + val log = envelope.getLastLog() assertNativeCrashSent(log, crashData, testRule.setup.symbols) } ) } + @Test + fun `native crash without session resurrected and sent properly`() { + val crashData = createStoredNativeCrashData( + serializer = serializer, + resourceFixtureName = "native_crash_1.txt", + crashMetadata = fakeNativeCrashStoredTelemetryMetadata, + ) + testRule.runTest( + instrumentedConfig = FakeInstrumentedConfig(enabledFeatures = FakeEnabledFeatureConfig(nativeCrashCapture = true)), + setupAction = { + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData) + setupFakeNativeCrash(serializer, crashData) + }, + testCaseAction = {}, + assertAction = { + val envelope = getSingleLogEnvelope() + with(envelope) { + assertEquals(fakeEnvelopeResource, resource) + assertEquals(fakeEnvelopeMetadata, metadata) + } + + val log = envelope.getLastLog() + assertNativeCrashSent(log, crashData, testRule.setup.symbols) + } + ) + } + + + @Test + fun `native crash without session or crash envelope is sent with current SDK envelope`() { + val crashData = createStoredNativeCrashData( + serializer = serializer, + resourceFixtureName = "native_crash_1.txt", + crashMetadata = fakeNativeCrashStoredTelemetryMetadata, + createCrashEnvelope = false, + ) + testRule.runTest( + instrumentedConfig = FakeInstrumentedConfig( + enabledFeatures = FakeEnabledFeatureConfig( + bgActivityCapture = false, + nativeCrashCapture = true + ) + ), + setupAction = { + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData) + setupFakeNativeCrash(serializer, crashData) + }, + testCaseAction = { + recordSession() + }, + assertAction = { + val session = getSingleSessionEnvelope() + val envelopes = getLogEnvelopes(2) + with(envelopes.first()) { + assertEquals(session.resource, resource) + assertEquals(session.metadata, metadata) + val crash = getLastLog() + assertNativeCrashSent(crash, crashData, testRule.setup.symbols) + } + + with(envelopes.last()) { + val errors = checkNotNull(data.logs) + assertEquals(2, errors.size) + with(errors.first()) { + assertEquals( + "Cached native crash envelope data not found", + attributes?.findAttributeValue(ExceptionAttributes.EXCEPTION_MESSAGE.key) + ) + } + + with(errors.last()) { + assertEquals( + "java.io.FileNotFoundException", + attributes?.findAttributeValue(ExceptionAttributes.EXCEPTION_TYPE.key) + ) + } + } + } + ) + } + + @Test + fun `session with native crash ID but no matching crash sent properly`() { + val crashData = createStoredNativeCrashData( + serializer = serializer, + resourceFixtureName = "native_crash_1.txt", + crashMetadata = fakeNativeCrashStoredTelemetryMetadata, + sessionMetadata = fakeCachedSessionStoredTelemetryMetadata, + ) + testRule.runTest( + instrumentedConfig = FakeInstrumentedConfig(enabledFeatures = FakeEnabledFeatureConfig(nativeCrashCapture = true)), + setupAction = { + setupCachedDataFromNativeCrash(cacheStorageService, crashData = crashData) + }, + testCaseAction = {}, + assertAction = { + with(getSingleSessionEnvelope()) { + assertDeadSessionResurrected(null) + } + assertEquals(0, getLogEnvelopes(0).size) + } + ) + } + @Test fun `empty crash envelope not available for native crash resurrection if background activity is enabled`() { testRule.runTest( @@ -109,7 +228,7 @@ internal class ResurrectionFeatureTest { getSessionEnvelopes(3) with(cacheStorageService.getCachedCrashEnvelope().single()) { assertEquals(SupportedEnvelopeType.CRASH, envelopeType) - assertEquals(PayloadType.NATIVE_CRASH, payloadType) + assertEquals(PayloadType.UNKNOWN, payloadType) assertFalse(complete) } diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/EmbracePayloadAssertionInterface.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/EmbracePayloadAssertionInterface.kt index 4444d763b0..344838f60a 100644 --- a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/EmbracePayloadAssertionInterface.kt +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/EmbracePayloadAssertionInterface.kt @@ -26,16 +26,16 @@ import io.embrace.android.embracesdk.internal.spans.findAttributeValue import io.embrace.android.embracesdk.testframework.assertions.JsonComparator import io.embrace.android.embracesdk.testframework.assertions.assertMatches import io.embrace.android.embracesdk.testframework.server.FakeApiServer -import java.io.File -import java.io.IOException -import java.util.Locale -import java.util.concurrent.TimeoutException import org.json.JSONObject import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +import java.io.File +import java.io.IOException +import java.util.Locale +import java.util.concurrent.TimeoutException /** * Provides assertions that can be used in integration tests to validate the behavior of the SDK, @@ -244,7 +244,7 @@ internal class EmbracePayloadAssertionInterface( assertEquals(Span.Status.ERROR, status) if (crashData != null) { - assertEquals(crashData.sessionEnvelope.getSessionId(), getSessionId()) + assertEquals(checkNotNull(crashData.sessionEnvelope).getSessionId(), getSessionId()) assertEquals(crashData.lastHeartbeatMs, endTimeNanos?.nanosToMillis()) assertEquals( crashData.nativeCrash.nativeCrashId, @@ -281,7 +281,9 @@ internal class EmbracePayloadAssertionInterface( ) assertNotNull(attrs.findAttributeValue("log.record.uid")) assertNotNull(attrs.findAttributeValue("emb.android.crash_number")) - assertEquals(crashData.sessionEnvelope.getSessionId(), attrs.findAttributeValue("session.id")) + if (crashData.sessionEnvelope != null) { + assertEquals(crashData.sessionEnvelope.getSessionId(), attrs.findAttributeValue("session.id")) + } assertFalse(crashData.getCrashFile().exists()) } diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/EmbraceSetupInterface.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/EmbraceSetupInterface.kt index 6ef881efa4..1029fbe204 100644 --- a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/EmbraceSetupInterface.kt +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/EmbraceSetupInterface.kt @@ -41,7 +41,12 @@ internal class EmbraceSetupInterface @JvmOverloads constructor( currentTimeMs: Long = IntegrationTestRule.DEFAULT_SDK_START_TIME_MS, var useMockWebServer: Boolean = true, val overriddenClock: FakeClock = FakeClock(currentTime = currentTimeMs), - val overriddenInitModule: FakeInitModule = FakeInitModule(clock = overriddenClock, logger = FakeEmbLogger()), + val processIdentifier: String = "integration-test-process", + val overriddenInitModule: FakeInitModule = FakeInitModule( + clock = overriddenClock, + logger = FakeEmbLogger(), + processIdentifierProvider = { processIdentifier } + ), val overriddenOpenTelemetryModule: OpenTelemetryModule = overriddenInitModule.openTelemetryModule, val overriddenCoreModule: FakeCoreModule = FakeCoreModule(), val overriddenWorkerThreadModule: WorkerThreadModule = createWorkerThreadModule(), @@ -128,11 +133,16 @@ internal class EmbraceSetupInterface @JvmOverloads constructor( /** * Setup a fake dead session on disk */ - fun EmbraceSetupInterface.setupFakeDeadSession( + fun EmbraceSetupInterface.setupCachedDataFromNativeCrash( storageService: FakePayloadStorageService, crashData: StoredNativeCrashData, ) { - storageService.addPayload(crashData.sessionMetadata, crashData.sessionEnvelope) + if (crashData.sessionMetadata != null && crashData.sessionEnvelope != null) { + storageService.addPayload(crashData.sessionMetadata, crashData.sessionEnvelope) + } + if (crashData.cachedCrashEnvelopeMetadata != null && crashData.cachedCrashEnvelope != null) { + storageService.addPayload(crashData.cachedCrashEnvelopeMetadata, crashData.cachedCrashEnvelope) + } cacheStorageServiceProvider = { storageService } } diff --git a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/StoredNdkData.kt b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/StoredNdkData.kt index f1f4d0f472..ca0ae43d7f 100644 --- a/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/StoredNdkData.kt +++ b/embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/actions/StoredNdkData.kt @@ -4,21 +4,30 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import io.embrace.android.embracesdk.ResourceReader import io.embrace.android.embracesdk.fakes.FakeEmbLogger +import io.embrace.android.embracesdk.fakes.fakeEmptyLogEnvelope +import io.embrace.android.embracesdk.fakes.fakeEnvelopeMetadata +import io.embrace.android.embracesdk.fakes.fakeEnvelopeResource import io.embrace.android.embracesdk.fakes.fakeIncompleteSessionEnvelope import io.embrace.android.embracesdk.internal.delivery.StoredTelemetryMetadata +import io.embrace.android.embracesdk.internal.delivery.SupportedEnvelopeType import io.embrace.android.embracesdk.internal.delivery.storage.StorageLocation import io.embrace.android.embracesdk.internal.payload.Envelope +import io.embrace.android.embracesdk.internal.payload.EnvelopeMetadata +import io.embrace.android.embracesdk.internal.payload.EnvelopeResource +import io.embrace.android.embracesdk.internal.payload.LogPayload import io.embrace.android.embracesdk.internal.payload.NativeCrashData import io.embrace.android.embracesdk.internal.payload.SessionPayload import io.embrace.android.embracesdk.internal.serialization.PlatformSerializer import java.io.File internal data class StoredNativeCrashData( - val sessionMetadata: StoredTelemetryMetadata, + val sessionMetadata: StoredTelemetryMetadata?, val crashMetadata: StoredTelemetryMetadata, + val cachedCrashEnvelopeMetadata: StoredTelemetryMetadata?, val nativeCrash: NativeCrashData, - val sessionEnvelope: Envelope, - val lastHeartbeatMs: Long = sessionMetadata.timestamp + 1000L, + val sessionEnvelope: Envelope?, + val cachedCrashEnvelope: Envelope?, + val lastHeartbeatMs: Long? = if (sessionMetadata != null) sessionMetadata.timestamp + 1000L else null, ) { fun getCrashFile(): File { @@ -32,9 +41,12 @@ internal data class StoredNativeCrashData( internal fun createStoredNativeCrashData( serializer: PlatformSerializer, - sessionMetadata: StoredTelemetryMetadata, - crashMetadata: StoredTelemetryMetadata, resourceFixtureName: String, + crashMetadata: StoredTelemetryMetadata, + createCrashEnvelope: Boolean = true, + sessionMetadata: StoredTelemetryMetadata? = null, + envelopeResource: EnvelopeResource = fakeEnvelopeResource, + envelopeMetadata: EnvelopeMetadata = fakeEnvelopeMetadata, ): StoredNativeCrashData { val nativeCrashData = serializer.fromJson( ResourceReader.readResource(resourceFixtureName), @@ -42,12 +54,38 @@ internal fun createStoredNativeCrashData( ) return StoredNativeCrashData( sessionMetadata = sessionMetadata, + cachedCrashEnvelopeMetadata = if (createCrashEnvelope) { + StoredTelemetryMetadata( + timestamp = crashMetadata.timestamp, + uuid = nativeCrashData.sessionId, + processId = crashMetadata.processId, + complete = false, + envelopeType = SupportedEnvelopeType.CRASH, + ) + } else { + null + }, nativeCrash = nativeCrashData, - sessionEnvelope = fakeIncompleteSessionEnvelope( - startMs = sessionMetadata.timestamp, - lastHeartbeatTimeMs = sessionMetadata.timestamp + 1000L, - sessionId = nativeCrashData.sessionId - ), - crashMetadata = crashMetadata + sessionEnvelope = if (sessionMetadata != null) { + fakeIncompleteSessionEnvelope( + startMs = sessionMetadata.timestamp, + lastHeartbeatTimeMs = sessionMetadata.timestamp + 1000L, + sessionId = nativeCrashData.sessionId, + processIdentifier = sessionMetadata.processId, + resource = envelopeResource, + metadata = envelopeMetadata + ) + } else { + null + }, + cachedCrashEnvelope = if (createCrashEnvelope) { + fakeEmptyLogEnvelope( + resource = envelopeResource, + metadata = envelopeMetadata + ) + } else { + null + }, + crashMetadata = crashMetadata, ) } diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeCachedLogEnvelopeStore.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeCachedLogEnvelopeStore.kt new file mode 100644 index 0000000000..88562e2be8 --- /dev/null +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeCachedLogEnvelopeStore.kt @@ -0,0 +1,32 @@ +package io.embrace.android.embracesdk.fakes + +import io.embrace.android.embracesdk.internal.delivery.StoredTelemetryMetadata +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore +import io.embrace.android.embracesdk.internal.payload.Envelope +import io.embrace.android.embracesdk.internal.payload.EnvelopeMetadata +import io.embrace.android.embracesdk.internal.payload.EnvelopeResource +import io.embrace.android.embracesdk.internal.payload.LogPayload + +class FakeCachedLogEnvelopeStore : CachedLogEnvelopeStore { + val envelopeGetRequest: MutableList = mutableListOf() + val createdEnvelopes: MutableList> = mutableListOf() + private val currentEnvelopes: MutableMap> = mutableMapOf() + + override fun create( + storedTelemetryMetadata: StoredTelemetryMetadata, + resource: EnvelopeResource, + metadata: EnvelopeMetadata, + ) { + val envelope = fakeEmptyLogEnvelope(resource = resource, metadata = metadata) + createdEnvelopes.add(envelope) + currentEnvelopes[storedTelemetryMetadata] = envelope + } + + override fun get(storedTelemetryMetadata: StoredTelemetryMetadata): Envelope? { + val envelope = currentEnvelopes[storedTelemetryMetadata] + envelopeGetRequest.add(storedTelemetryMetadata) + return envelope + } + + override fun clear() = currentEnvelopes.clear() +} diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogEnvelopeSource.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogEnvelopeSource.kt index a67f19acb5..c2efd6f51c 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogEnvelopeSource.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogEnvelopeSource.kt @@ -6,6 +6,7 @@ import io.embrace.android.embracesdk.internal.envelope.metadata.EnvelopeMetadata import io.embrace.android.embracesdk.internal.envelope.resource.EnvelopeResourceSource import io.embrace.android.embracesdk.internal.logs.LogRequest import io.embrace.android.embracesdk.internal.payload.Envelope +import io.embrace.android.embracesdk.internal.payload.Envelope.Companion.createLogEnvelope import io.embrace.android.embracesdk.internal.payload.LogPayload class FakeLogEnvelopeSource( @@ -27,11 +28,8 @@ class FakeLogEnvelopeSource( override fun getEmptySingleLogEnvelope(): Envelope = getLogEnvelope(LogPayload()) - private fun getLogEnvelope(payload: LogPayload) = Envelope( - resourceSource.getEnvelopeResource(), - metadataSource.getEnvelopeMetadata(), - "0.1.0", - "logs", - payload + private fun getLogEnvelope(payload: LogPayload) = payload.createLogEnvelope( + resource = resourceSource.getEnvelopeResource(), + metadata = metadataSource.getEnvelopeMetadata(), ) } diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogFixtures.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogFixtures.kt index 69d6216d7a..e97c308b02 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogFixtures.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogFixtures.kt @@ -1,14 +1,36 @@ package io.embrace.android.embracesdk.fakes -import io.embrace.android.embracesdk.internal.payload.Envelope +import io.embrace.android.embracesdk.internal.payload.AppFramework +import io.embrace.android.embracesdk.internal.payload.Envelope.Companion.createLogEnvelope import io.embrace.android.embracesdk.internal.payload.EnvelopeMetadata import io.embrace.android.embracesdk.internal.payload.EnvelopeResource import io.embrace.android.embracesdk.internal.payload.LogPayload -fun fakeEmptyLogEnvelope() = Envelope( - resource = EnvelopeResource(), - metadata = EnvelopeMetadata(), - version = "1.0.0", - type = "logs", - data = LogPayload() +fun fakeEmptyLogEnvelope( + resource: EnvelopeResource = fakeEnvelopeResource, + metadata: EnvelopeMetadata = fakeEnvelopeMetadata, +) = LogPayload().createLogEnvelope(resource = resource, metadata = metadata) + +val fakeEnvelopeResource = EnvelopeResource( + appVersion = "1.5", + appFramework = AppFramework.NATIVE, + buildId = "555", + appEcosystemId = "com.my.app", + sdkVersion = "6.14.0", + deviceManufacturer = "Google", + deviceModel = "Pixel 5", + osType = "linux", + osName = "Android", + osVersion = "15", ) + +val fakeLaterEnvelopeResource = fakeEnvelopeResource.copy(appVersion = "1.6") + +val fakeEnvelopeMetadata = EnvelopeMetadata( + userId = "abcde", + email = "me@mycompany.com", + username = "admin", + personas = setOf("stuff-doer"), +) + +val fakeLaterEnvelopeMetadata = fakeEnvelopeMetadata.copy(username = "new-admin") diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogPayloadSource.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogPayloadSource.kt index 7888bc647d..e9ece9543f 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogPayloadSource.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeLogPayloadSource.kt @@ -1,17 +1,18 @@ package io.embrace.android.embracesdk.fakes import io.embrace.android.embracesdk.fixtures.sendImmediatelyLog +import io.embrace.android.embracesdk.fixtures.testLog import io.embrace.android.embracesdk.internal.envelope.log.LogPayloadSource import io.embrace.android.embracesdk.internal.logs.LogRequest import io.embrace.android.embracesdk.internal.payload.LogPayload class FakeLogPayloadSource : LogPayloadSource { - var singleLogPayloadsSource: List> = listOf(LogRequest(LogPayload(logs = listOf(sendImmediatelyLog)))) - private val batchedLogPayload: LogPayload = LogPayload() - override fun getBatchedLogPayload(): LogPayload = batchedLogPayload + var batchedLogPayloadSource: LogPayload = LogPayload(logs = listOf(testLog)) + + override fun getBatchedLogPayload(): LogPayload = batchedLogPayloadSource override fun getSingleLogPayloads(): List> = singleLogPayloadsSource } diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakePersistableEmbraceSpan.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakePersistableEmbraceSpan.kt index 3ef4453be6..2c2eaaeb30 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakePersistableEmbraceSpan.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakePersistableEmbraceSpan.kt @@ -10,6 +10,7 @@ import io.embrace.android.embracesdk.internal.clock.millisToNanos import io.embrace.android.embracesdk.internal.clock.normalizeTimestampAsMillis import io.embrace.android.embracesdk.internal.config.instrumented.InstrumentedConfigImpl import io.embrace.android.embracesdk.internal.opentelemetry.embHeartbeatTimeUnixNano +import io.embrace.android.embracesdk.internal.opentelemetry.embProcessIdentifier import io.embrace.android.embracesdk.internal.opentelemetry.embState import io.embrace.android.embracesdk.internal.payload.Span import io.embrace.android.embracesdk.internal.payload.toNewPayload @@ -202,6 +203,7 @@ class FakePersistableEmbraceSpan( lastHeartbeatTimeMs: Long?, endTimeMs: Long? = null, sessionProperties: Map? = null, + processIdentifier: String = "fake-process-id" ): FakePersistableEmbraceSpan = FakePersistableEmbraceSpan( name = "emb-session", @@ -216,6 +218,7 @@ class FakePersistableEmbraceSpan( } setSystemAttribute(SessionIncubatingAttributes.SESSION_ID, sessionId) + setSystemAttribute(embProcessIdentifier.attributeKey, processIdentifier) setSystemAttribute(embState.attributeKey, "foreground") setSystemAttribute( embHeartbeatTimeUnixNano.attributeKey, diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeSession.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeSession.kt index 04f94260fd..046239becf 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeSession.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/FakeSession.kt @@ -35,8 +35,8 @@ fun fakeSessionEnvelope( val spanSnapshots = listOfNotNull(FakePersistableEmbraceSpan.started().snapshot()) return Envelope( - resource = EnvelopeResource(), - metadata = EnvelopeMetadata(), + resource = fakeEnvelopeResource, + metadata = fakeEnvelopeMetadata, version = "1.0.0", type = "spans", data = SessionPayload( @@ -48,9 +48,12 @@ fun fakeSessionEnvelope( fun fakeIncompleteSessionEnvelope( sessionId: String = "fakeIncompleteSessionId", + processIdentifier: String = "fakeIncompleteSessionProcessId", startMs: Long = 1691000000000L, lastHeartbeatTimeMs: Long = 1691000300000L, sessionProperties: Map? = null, + resource: EnvelopeResource = fakeEnvelopeResource, + metadata: EnvelopeMetadata = fakeEnvelopeMetadata, ): Envelope { val fakeClock = FakeClock(currentTime = startMs) val incompleteSessionSpan = FakePersistableEmbraceSpan.sessionSpan( @@ -58,10 +61,11 @@ fun fakeIncompleteSessionEnvelope( startTimeMs = startMs, lastHeartbeatTimeMs = lastHeartbeatTimeMs, sessionProperties = sessionProperties, + processIdentifier = processIdentifier ) return Envelope( - resource = EnvelopeResource(), - metadata = EnvelopeMetadata(), + resource = resource, + metadata = metadata, version = "1.0.0", type = "spans", data = SessionPayload( diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/injection/FakeDeliveryModule.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/injection/FakeDeliveryModule.kt index f7fb403ebe..82bcaf5b90 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/injection/FakeDeliveryModule.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/injection/FakeDeliveryModule.kt @@ -1,5 +1,6 @@ package io.embrace.android.embracesdk.fakes.injection +import io.embrace.android.embracesdk.fakes.FakeCachedLogEnvelopeStore import io.embrace.android.embracesdk.fakes.FakeDeliveryService import io.embrace.android.embracesdk.fakes.FakeIntakeService import io.embrace.android.embracesdk.fakes.FakePayloadCachingService @@ -12,6 +13,7 @@ import io.embrace.android.embracesdk.internal.delivery.debug.DeliveryTracer import io.embrace.android.embracesdk.internal.delivery.execution.RequestExecutionService import io.embrace.android.embracesdk.internal.delivery.intake.IntakeService import io.embrace.android.embracesdk.internal.delivery.scheduling.SchedulingService +import io.embrace.android.embracesdk.internal.delivery.storage.CachedLogEnvelopeStore import io.embrace.android.embracesdk.internal.delivery.storage.PayloadStorageService import io.embrace.android.embracesdk.internal.injection.DeliveryModule import io.embrace.android.embracesdk.internal.session.orchestrator.PayloadStore @@ -23,7 +25,8 @@ class FakeDeliveryModule( override val payloadCachingService: PayloadCachingService = FakePayloadCachingService(), override val payloadStorageService: PayloadStorageService = FakePayloadStorageService(), override val cacheStorageService: PayloadStorageService = FakePayloadStorageService(), + override val cachedLogEnvelopeStore: CachedLogEnvelopeStore? = FakeCachedLogEnvelopeStore(), override val requestExecutionService: RequestExecutionService = FakeRequestExecutionService(), override val schedulingService: SchedulingService = FakeSchedulingService(), - override val deliveryTracer: DeliveryTracer? = null + override val deliveryTracer: DeliveryTracer? = null, ) : DeliveryModule diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/injection/FakeInitModule.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/injection/FakeInitModule.kt index cc7ea9c2ec..4061e99b3c 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/injection/FakeInitModule.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/injection/FakeInitModule.kt @@ -23,9 +23,10 @@ class FakeInitModule( initModule: InitModule = createInitModule( clock = clock, logger = logger, - systemInfo = systemInfo + systemInfo = systemInfo, ), - override var instrumentedConfig: InstrumentedConfig = FakeInstrumentedConfig() + override var instrumentedConfig: InstrumentedConfig = FakeInstrumentedConfig(), + override val processIdentifierProvider: () -> String = { "fake-process-id" }, ) : InitModule by initModule { val openTelemetryModule: OpenTelemetryModule by lazy { createOpenTelemetryModule(initModule) } diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fixtures/DeliveryFixtures.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fixtures/DeliveryFixtures.kt index f37e70fb83..6ef71d7e07 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fixtures/DeliveryFixtures.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fixtures/DeliveryFixtures.kt @@ -40,8 +40,8 @@ val fakeLogStoredTelemetryMetadata = StoredTelemetryMetadata( ) val fakeNativeCrashStoredTelemetryMetadata = StoredTelemetryMetadata( - timestamp = DEFAULT_FAKE_CURRENT_TIME + 500L, - uuid = "794b67fd-2dd7-4380-beeb-89c2432f25aa", + timestamp = 1681972471806L, + uuid = "bb6b5b1ea2ff48928382fe81d7991ced", processId = "8115ec91-3e5e-4d8a-816d-cc40306f9822", envelopeType = SupportedEnvelopeType.CRASH, complete = false, diff --git a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fixtures/LogTestFixtures.kt b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fixtures/LogTestFixtures.kt index 1747fded85..279fde89d9 100644 --- a/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fixtures/LogTestFixtures.kt +++ b/embrace-test-fakes/src/main/kotlin/io/embrace/android/embracesdk/fixtures/LogTestFixtures.kt @@ -1,11 +1,15 @@ package io.embrace.android.embracesdk.fixtures import io.embrace.android.embracesdk.fakes.FakeLogRecordData +import io.embrace.android.embracesdk.internal.arch.schema.EmbType import io.embrace.android.embracesdk.internal.arch.schema.SendMode +import io.embrace.android.embracesdk.internal.arch.schema.toPayload +import io.embrace.android.embracesdk.internal.opentelemetry.embProcessIdentifier import io.embrace.android.embracesdk.internal.opentelemetry.embSendMode import io.embrace.android.embracesdk.internal.payload.Attribute import io.embrace.android.embracesdk.internal.payload.Log import io.embrace.android.embracesdk.internal.payload.NativeCrashData +import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes val testLog: Log = Log( traceId = "ceadd56622414a06ae382e4e5a70bcf7", @@ -48,3 +52,21 @@ val testNativeCrashData: NativeCrashData = NativeCrashData( crash = "base64binarystring", symbols = mapOf("key" to "value"), ) + +val nativeCrashLog = Log( + timeUnixNano = 1681972471806000000L, + attributes = listOf( + Attribute(embSendMode.name, SendMode.IMMEDIATE.name), + EmbType.System.NativeCrash.toPayload(), + Attribute(SessionIncubatingAttributes.SESSION_ID.key, "bb6b5b1ea2ff48928382fe81d7991ced"), + Attribute(embProcessIdentifier.name, "8115ec91-3e5e-4d8a-816d-cc40306f9822"), + ) +) + +val nativeCrashWithoutSessionLog = Log( + timeUnixNano = 1681972481806000000L, + attributes = listOf( + Attribute(embSendMode.name, SendMode.IMMEDIATE.name), + EmbType.System.NativeCrash.toPayload(), + ) +)