Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.embrace.android.embracesdk.internal.arch.schema

import io.embrace.android.embracesdk.internal.opentelemetry.embAeiNumber
import io.embrace.android.embracesdk.internal.opentelemetry.embCrashNumber
import io.embrace.android.embracesdk.internal.opentelemetry.embSendMode
import io.embrace.android.embracesdk.internal.payload.AppExitInfoData
import io.embrace.android.embracesdk.internal.payload.NetworkCapturedCall
Expand Down Expand Up @@ -50,9 +52,7 @@ sealed class SchemaType(

/**
* Represents a push notification event.
* @param viewName The name of the view that the tap event occurred in.
* @param type The type of tap event. "tap"/"long_press". "tap" is the default.
* @param coords The coordinates of the tap event.
*/
class PushNotification(
title: String?,
Expand Down Expand Up @@ -111,10 +111,14 @@ sealed class SchemaType(
telemetryType = EmbType.Performance.MemoryWarning,
fixedObjectName = "memory-warning"
) {
override val schemaAttributes: Map<String, String> = emptyMap<String, String>()
override val schemaAttributes: Map<String, String> = emptyMap()
}

class AeiLog(message: AppExitInfoData) : SchemaType(EmbType.System.Exit) {
class AeiLog(
message: AppExitInfoData,
crashNumber: Int?,
aeiNumber: Int?,
) : SchemaType(EmbType.System.Exit) {
override val schemaAttributes: Map<String, String> = mapOf(
"aei_session_id" to message.sessionId,
"session_id_error" to message.sessionIdError,
Expand All @@ -125,7 +129,9 @@ sealed class SchemaType(
"exit_status" to message.status.toString(),
"timestamp" to message.timestamp.toString(),
"description" to message.description,
"trace_status" to message.traceStatus
"trace_status" to message.traceStatus,
embCrashNumber.attributeKey.key to crashNumber.toString(),
embAeiNumber.attributeKey.key to aeiNumber.toString()
).toNonNullMap()
}

Expand Down Expand Up @@ -165,7 +171,7 @@ sealed class SchemaType(
telemetryType = EmbType.System.LowPower,
fixedObjectName = "device-low-power"
) {
override val schemaAttributes: Map<String, String> = emptyMap<String, String>()
override val schemaAttributes: Map<String, String> = emptyMap()
}

class NetworkCapturedRequest(networkCapturedCall: NetworkCapturedCall) : SchemaType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,14 @@ class AppExitInfoBehaviorImpl(
/**
* Max size of bytes to allow capturing AppExitInfo ndk/anr traces
*/
private const val MAX_TRACE_SIZE_BYTES = 2097152 // 2MB
private const val MAX_TRACE_SIZE_BYTES = 10485760 // 10MB
const val AEI_MAX_NUM_DEFAULT: Int = 0 // 0 means no limit
}

override val local = local.enabledFeatures
override val remote = remote?.appExitInfoConfig

override fun getTraceMaxLimit(): Int =
remote?.appExitInfoTracesLimit
?: MAX_TRACE_SIZE_BYTES
override fun getTraceMaxLimit(): Int = remote?.appExitInfoTracesLimit ?: MAX_TRACE_SIZE_BYTES

override fun isAeiCaptureEnabled(): Boolean {
return thresholdCheck.isBehaviorEnabled(remote?.pctAeiCaptureEnabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ val embAndroidThreads: EmbraceAttributeKey = EmbraceAttributeKey("android.thread
*/
val embCrashNumber: EmbraceAttributeKey = EmbraceAttributeKey("android.crash_number")

/**
* Sequence number for the number of AEI crashes captured by Embrace on the device, reported on every AEI crash
*/
val embAeiNumber: EmbraceAttributeKey = EmbraceAttributeKey("android.aei_crash_number")

/**
* Attribute name for the exception handling type - whether it's handled or unhandled
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ internal class EmbracePreferencesService(
return incrementAndGetOrdinal(LAST_NATIVE_CRASH_NUMBER_KEY)
}

override fun incrementAndGetAeiCrashNumber(): Int {
return incrementAndGetOrdinal(LAST_AEI_CRASH_NUMBER_KEY)
}

private fun incrementAndGetOrdinal(key: String): Int {
return try {
val ordinal = (prefs.getIntegerPreference(key) ?: 0) + 1
Expand Down Expand Up @@ -278,6 +282,7 @@ internal class EmbracePreferencesService(
private const val LAST_BACKGROUND_ACTIVITY_NUMBER_KEY = "io.embrace.bgactivitynumber"
private const val LAST_CRASH_NUMBER_KEY = "io.embrace.crashnumber"
private const val LAST_NATIVE_CRASH_NUMBER_KEY = "io.embrace.nativecrashnumber"
private const val LAST_AEI_CRASH_NUMBER_KEY = "io.embrace.aeicrashnumber"
private const val JAVA_SCRIPT_BUNDLE_URL_KEY = "io.embrace.jsbundle.url"
private const val JAVA_SCRIPT_BUNDLE_ID_KEY = "io.embrace.jsbundle.id"
private const val JAVA_SCRIPT_PATCH_NUMBER_KEY = "io.embrace.javascript.patch"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ interface PreferencesService {
*/
fun incrementAndGetNativeCrashNumber(): Int

/**
* Increments and returns the AEI crash number ordinal. This is an integer that
* increments on every NDK tombstone detected via an AEI trace. It allows us to check the % of AEI crashes that
* didn't get delivered to the backend.
*/
fun incrementAndGetAeiCrashNumber(): Int

/**
* Last javaScript bundle string url.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal class AppExitInfoBehaviorImplTest {
@Test
fun testDefaults() {
with(createAppExitInfoBehavior()) {
assertEquals(2097152, getTraceMaxLimit())
assertEquals(10485760, getTraceMaxLimit())
assertTrue(isAeiCaptureEnabled())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,14 @@ internal class EmbracePreferencesServiceTest {
assertEquals(4, service.incrementAndGetNativeCrashNumber())
}

@Test
fun `test aei crash number is saved`() {
assertEquals(1, service.incrementAndGetAeiCrashNumber())
assertEquals(2, service.incrementAndGetAeiCrashNumber())
assertEquals(3, service.incrementAndGetAeiCrashNumber())
assertEquals(4, service.incrementAndGetAeiCrashNumber())
}

@Test
fun `test incrementAndGet returns -1 on an exception`() {
service = EmbracePreferencesService(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.embrace.android.embracesdk.internal.arch.schema.SchemaType.AeiLog
import io.embrace.android.embracesdk.internal.config.behavior.AppExitInfoBehavior
import io.embrace.android.embracesdk.internal.logging.EmbLogger
import io.embrace.android.embracesdk.internal.logging.InternalErrorType
import io.embrace.android.embracesdk.internal.payload.AppExitInfoData
import io.embrace.android.embracesdk.internal.prefs.PreferencesService
import io.embrace.android.embracesdk.internal.spans.toOtelSeverity
import io.embrace.android.embracesdk.internal.utils.BuildVersionChecker
Expand All @@ -35,7 +36,7 @@ internal class AeiDataSourceImpl(
) {

private companion object {
private const val SDK_AEI_SEND_LIMIT = 32
private const val SDK_AEI_SEND_LIMIT = 64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this limit apply for all AEIs? Any chance we can make it so that native crashes don't respect this limit?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the maximum number of AEI we request from the OS on each process launch. In practice AEI is only recorded after a process terminates so most of the time I'd expect the number of AEI sent to equal 1.

So assuming Embrace is enabled 100% of the time, the SDK will get 64 process launches where it can attempt to capture data before dropping anything. I wouldn't like to make this unbounded as there is a resource cost in capturing each AEI object

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And unfortunately the Android API doesn't provide a way to query just for crashes/ANRs, which would make our lives much simpler...

}

override fun enableDataCapture() {
Expand All @@ -54,17 +55,31 @@ internal class AeiDataSourceImpl(
val unsentRecords = getUnsentRecords(records)

unsentRecords.forEach {
val obj = it.constructAeiObject(versionChecker, appExitInfoBehavior.getTraceMaxLimit()) ?: return@forEach
val obj = it.constructAeiObject(versionChecker, appExitInfoBehavior.getTraceMaxLimit())
val crashNumber = obj?.getOrdinal(preferencesService::incrementAndGetCrashNumber)
val aeiNumber = obj?.getOrdinal(preferencesService::incrementAndGetAeiCrashNumber)
if (obj == null) {
return@forEach
}
captureData(
inputValidation = NoInputValidation,
captureAction = {
val schemaType = AeiLog(obj)
val schemaType = AeiLog(obj, crashNumber, aeiNumber)
addLog(schemaType, INFO.toOtelSeverity(), obj.trace ?: "")
}
)
}
}

private fun AppExitInfoData.hasNativeTombstone(): Boolean {
return trace != null && reason == ApplicationExitInfo.REASON_CRASH_NATIVE
}

private inline fun AppExitInfoData.getOrdinal(provider: () -> Int) = when (hasNativeTombstone()) {
true -> provider()
else -> null
}

/**
* Calculates what AEI records have been sent by subtracting a collection of IDs that have been previously
* sent from the return value of getHistoricalProcessExitReasons.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ internal class AeiDataSourceImplTest {
@Before
fun setUp() {
configService = FakeConfigService(
appExitInfoBehavior = FakeAppExitInfoBehavior(enabled = true)
appExitInfoBehavior = FakeAppExitInfoBehavior()
)
}

Expand Down Expand Up @@ -133,24 +133,24 @@ internal class AeiDataSourceImplTest {
}

@Test
fun `getHistoricalProcessExitInfo should truncate to 32 entries`() {
// given getHistoricalProcessExitReasons returns more than 32 entries
val appExitInfoListWithMoreThan32Entries = mutableListOf<ApplicationExitInfo>()
repeat(33) {
appExitInfoListWithMoreThan32Entries.add(mockAppExitInfo)
fun `getHistoricalProcessExitInfo should truncate to 64 entries`() {
// given getHistoricalProcessExitReasons returns more than 64 entries
val entries = mutableListOf<ApplicationExitInfo>()
repeat(65) {
entries.add(mockAppExitInfo)
}
every {
mockActivityManager.getHistoricalProcessExitReasons(
any(),
any(),
any()
)
} returns appExitInfoListWithMoreThan32Entries
} returns entries

startApplicationExitInfoService()

// then captured data should only have 32 entries
assertEquals(32, logWriter.logEvents.size)
// then captured data should only have 64 entries
assertEquals(64, logWriter.logEvents.size)
}

@Test
Expand Down Expand Up @@ -335,7 +335,7 @@ internal class AeiDataSourceImplTest {

@Test
fun `one object sent per payload`() {
val entries = (0..32).map { mockAppExitInfo }
val entries = (0..64).map { mockAppExitInfo }
every {
mockActivityManager.getHistoricalProcessExitReasons(
any(),
Expand All @@ -347,7 +347,7 @@ internal class AeiDataSourceImplTest {
startApplicationExitInfoService()

// each AEI object with a trace should be sent in a separate payload
assertEquals(32, logWriter.logEvents.size)
assertEquals(64, logWriter.logEvents.size)
}

private fun getAeiLogAttrs(): Map<String, String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import io.embrace.android.embracesdk.internal.arch.schema.EmbType
import io.embrace.android.embracesdk.internal.config.remote.AppExitInfoConfig
import io.embrace.android.embracesdk.internal.config.remote.RemoteConfig
import io.embrace.android.embracesdk.internal.logging.InternalErrorType
import io.embrace.android.embracesdk.internal.opentelemetry.embAeiNumber
import io.embrace.android.embracesdk.internal.opentelemetry.embCrashNumber
import io.embrace.android.embracesdk.internal.payload.Log
import io.embrace.android.embracesdk.internal.spans.findAttributeValue
import io.embrace.android.embracesdk.testframework.SdkIntegrationTestRule
import io.embrace.android.embracesdk.testframework.actions.EmbraceSetupInterface
import io.embrace.android.embracesdk.testframework.assertions.assertMatches
import io.embrace.android.embracesdk.testframework.assertions.getLastLog
import io.embrace.android.embracesdk.testframework.assertions.getLogOfType
import io.embrace.android.embracesdk.testframework.assertions.getLogsOfType
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -123,14 +126,15 @@ internal class AeiFeatureTest {
fun `native crash`() {
testRule.runTest(
setupAction = {
setupFakeAeiData(listOf(nativeCrash.toAeiObject()))
setupFakeAeiData(listOf(nativeCrash.toAeiObject(), nativeCrash.toAeiObject()))
},
testCaseAction = {
recordSession()
},
assertAction = {
val log = getSingleLogEnvelope().getLogOfType(EmbType.System.Exit)
log.assertContainsAeiData(nativeCrash)
val logs = getLogEnvelopes(2).flatMap { it.getLogsOfType(EmbType.System.Exit) }
logs[0].assertContainsAeiData(nativeCrash, "1", "1")
logs[1].assertContainsAeiData(nativeCrash, "2", "2")
}
)
}
Expand Down Expand Up @@ -169,9 +173,9 @@ internal class AeiFeatureTest {

@Test
fun `aei limit exceeded`() {
val timestamps = 0..50L
val timestamps = 0..100L
val aeis = timestamps.map { anr.copy(timestamp = it) }.map(TestAeiData::toAeiObject)
val expectedSize = 32
val expectedSize = 64

testRule.runTest(
setupAction = {
Expand All @@ -185,7 +189,7 @@ internal class AeiFeatureTest {
assertEquals(expectedSize, envelopes.size)
val logs = envelopes.mapNotNull { it.data.logs?.singleOrNull() }
.filter { it.attributes?.findAttributeValue("emb.type") == "sys.exit" }
assertEquals(32, logs.size)
assertEquals(expectedSize, logs.size)
}
)
}
Expand Down Expand Up @@ -291,6 +295,8 @@ internal class AeiFeatureTest {

private fun Log.assertContainsAeiData(
expected: TestAeiData,
crashNumber: String = "null",
aeiNumber: String = "null",
) {
with(expected) {
attributes?.assertMatches(
Expand All @@ -305,7 +311,9 @@ internal class AeiFeatureTest {
"exit_status" to status,
"description" to description,
"reason" to reason,
"emb.type" to "sys.exit"
"emb.type" to "sys.exit",
embCrashNumber.attributeKey.key to crashNumber,
embAeiNumber.attributeKey.key to aeiNumber,
)
)
assertEquals(trace, body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,9 @@ class FakePreferenceService(
return 1
}

override fun incrementAndGetAeiCrashNumber(): Int {
return 1
}

override fun isUsersFirstDay(): Boolean = firstDay
}
Loading