diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/EmbType.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/EmbType.kt index 4107e09f54..f45b34b3ee 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/EmbType.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/EmbType.kt @@ -57,6 +57,8 @@ internal sealed class EmbType(type: String, subtype: String?) : TelemetryType { internal object Exception : System("exception") + internal object InternalError : System("internal") + internal object FlutterException : System("flutter_exception") { /** * Attribute name for the exception context in a log representing an exception diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt index 7629b0f7d8..80bb74be6b 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/arch/schema/SchemaType.kt @@ -347,4 +347,15 @@ internal sealed class SchemaType( "status" to status.toString() ) } + + internal class InternalError(throwable: Throwable) : SchemaType( + telemetryType = EmbType.System.InternalError, + fixedObjectName = "internal-error" + ) { + override val schemaAttributes = mapOf( + "exception.type" to throwable.javaClass.name, + "exception.stacktrace" to throwable.stackTrace.joinToString("\n", transform = StackTraceElement::toString), + "exception.message" to (throwable.message ?: "") + ) + } } diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorDataSource.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorDataSource.kt new file mode 100644 index 0000000000..35f867dd53 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorDataSource.kt @@ -0,0 +1,5 @@ +package io.embrace.android.embracesdk.capture.internal.errors + +import io.embrace.android.embracesdk.arch.datasource.LogDataSource + +internal interface InternalErrorDataSource : LogDataSource, InternalErrorHandler diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorDataSourceImpl.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorDataSourceImpl.kt new file mode 100644 index 0000000000..64e6449816 --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorDataSourceImpl.kt @@ -0,0 +1,33 @@ +package io.embrace.android.embracesdk.capture.internal.errors + +import io.embrace.android.embracesdk.Severity +import io.embrace.android.embracesdk.arch.datasource.LogDataSourceImpl +import io.embrace.android.embracesdk.arch.datasource.NoInputValidation +import io.embrace.android.embracesdk.arch.destination.LogEventData +import io.embrace.android.embracesdk.arch.destination.LogWriter +import io.embrace.android.embracesdk.arch.limits.UpToLimitStrategy +import io.embrace.android.embracesdk.arch.schema.SchemaType +import io.embrace.android.embracesdk.logging.EmbLogger + +/** + * Tracks internal errors & sends them as OTel logs. + */ +internal class InternalErrorDataSourceImpl( + logWriter: LogWriter, + logger: EmbLogger, +) : InternalErrorDataSource, + LogDataSourceImpl( + destination = logWriter, + logger = logger, + limitStrategy = UpToLimitStrategy { 10 }, + ) { + + override fun handleInternalError(throwable: Throwable) { + alterSessionSpan(NoInputValidation) { + this.addLog(throwable, true) { + val schemaType = SchemaType.InternalError(throwable) + LogEventData(schemaType, Severity.ERROR, "") + } + } + } +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorHandler.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorHandler.kt new file mode 100644 index 0000000000..86e36ff97c --- /dev/null +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorHandler.kt @@ -0,0 +1,5 @@ +package io.embrace.android.embracesdk.capture.internal.errors + +internal interface InternalErrorHandler { + fun handleInternalError(throwable: Throwable) +} diff --git a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorService.kt b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorService.kt index 40e16c6223..067bbac15e 100644 --- a/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorService.kt +++ b/embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/capture/internal/errors/InternalErrorService.kt @@ -8,7 +8,6 @@ import io.embrace.android.embracesdk.payload.LegacyExceptionError * Reports an internal error to Embrace. An internal error is defined as an exception that was * caught within Embrace code & logged to [EmbLogger]. */ -internal interface InternalErrorService : DataCaptureService { +internal interface InternalErrorService : DataCaptureService, InternalErrorHandler { var configService: ConfigService? - fun handleInternalError(throwable: Throwable) } diff --git a/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crash/InternalErrorDataSourceImplTest.kt b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crash/InternalErrorDataSourceImplTest.kt new file mode 100644 index 0000000000..6c09e51d9a --- /dev/null +++ b/embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/capture/crash/InternalErrorDataSourceImplTest.kt @@ -0,0 +1,69 @@ +package io.embrace.android.embracesdk.capture.crash + +import io.embrace.android.embracesdk.Severity +import io.embrace.android.embracesdk.arch.destination.LogEventData +import io.embrace.android.embracesdk.arch.schema.EmbType +import io.embrace.android.embracesdk.capture.internal.errors.InternalErrorDataSourceImpl +import io.embrace.android.embracesdk.fakes.FakeLogWriter +import io.embrace.android.embracesdk.logging.EmbLogger +import io.embrace.android.embracesdk.logging.EmbLoggerImpl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test + +internal class InternalErrorDataSourceImplTest { + + private lateinit var dataSource: InternalErrorDataSourceImpl + private lateinit var logWriter: FakeLogWriter + private lateinit var logger: EmbLogger + + @Before + fun setUp() { + logWriter = FakeLogWriter() + logger = EmbLoggerImpl() + dataSource = InternalErrorDataSourceImpl( + logWriter, + logger + ) + } + + @Test + fun `handle throwable with no message`() { + dataSource.handleInternalError(IllegalStateException()) + val data = logWriter.logEvents.single() + val attrs = assertInternalErrorLogged(data) + assertEquals("java.lang.IllegalStateException", attrs["exception.type"]) + assertEquals("", attrs["exception.message"]) + assertNotNull(attrs["exception.stacktrace"]) + } + + @Test + fun `handle throwable with message`() { + dataSource.handleInternalError(IllegalArgumentException("Whoops!")) + val data = logWriter.logEvents.single() + val attrs = assertInternalErrorLogged(data) + assertEquals("java.lang.IllegalArgumentException", attrs["exception.type"]) + assertEquals("Whoops!", attrs["exception.message"]) + assertNotNull(attrs["exception.stacktrace"]) + } + + @Test + fun `limit not exceeded`() { + repeat(15) { + dataSource.handleInternalError(IllegalStateException()) + } + assertEquals(10, logWriter.logEvents.size) + } + + private fun assertInternalErrorLogged(data: LogEventData): Map { + assertEquals(Severity.ERROR, data.severity) + assertEquals("", data.message) + assertEquals(EmbType.System.InternalError, data.schemaType.telemetryType) + assertEquals("internal-error", data.schemaType.fixedObjectName) + + val attrs = data.schemaType.attributes() + assertEquals("sys.internal", attrs["emb.type"]) + return attrs + } +}