Skip to content

Commit 44d9ea1

Browse files
committed
Unify OTel and Embrace envelope resource attributes
1 parent dcecf31 commit 44d9ea1

36 files changed

Lines changed: 573 additions & 682 deletions

File tree

embrace-android-api/src/main/kotlin/io/embrace/android/embracesdk/internal/api/OTelApi.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,13 @@ public interface OTelApi {
4343

4444
/**
4545
* Set an attribute on the resource used by the OTel SDK instance with the given String key and value.
46-
* The value set will override any value set previously or by the Embrace SDK.
4746
* This must be called before the SDK is started in order for it to take effect.
47+
*
48+
* Resource attributes that the Embrace SDK sets will not be overridden by this call. A configuration parameter is required
49+
* for that to happen as a safety mechanism for the inadvertent use of the same resource attributes that the SDK relies on.
50+
*
51+
* Attributes in the 'emb.' namespace (i.e. prefixed by 'emb.') are reserved for the Embrace SDK and can never be set or
52+
* overridden by this method, regardless of the override configuration.
4853
*/
4954
public fun setResourceAttribute(key: String, value: String)
5055
}

embrace-android-config-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/config/FakeEnabledFeatureConfig.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class FakeEnabledFeatureConfig(
3232
private val uiLoadTracingTraceAll: Boolean = base.isUiLoadTracingTraceAll(),
3333
private val endStartupWithAppReady: Boolean = base.isEndStartupWithAppReadyEnabled(),
3434
private val otelKotlinSdkEnabled: Boolean = base.isOtelKotlinSdkEnabled(),
35+
private val resourceAttributeOverride: Boolean = base.isResourceAttributeOverrideEnabled(),
3536
) : EnabledFeatureConfig {
3637

3738
override fun isActivityBreadcrumbCaptureEnabled(): Boolean = activityBreadcrumbCapture
@@ -59,4 +60,5 @@ class FakeEnabledFeatureConfig(
5960
override fun isUiLoadTracingTraceAll(): Boolean = uiLoadTracingTraceAll
6061
override fun isEndStartupWithAppReadyEnabled(): Boolean = endStartupWithAppReady
6162
override fun isOtelKotlinSdkEnabled(): Boolean = otelKotlinSdkEnabled
63+
override fun isResourceAttributeOverrideEnabled(): Boolean = resourceAttributeOverride
6264
}

embrace-android-config-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/config/FakeInstrumentedConfig.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ data class FakeInstrumentedConfig(
1717
override val enabledFeatures: FakeEnabledFeatureConfig = FakeEnabledFeatureConfig(base.enabledFeatures),
1818
override val networkCapture: NetworkCaptureConfig = FakeNetworkCaptureConfig(base.networkCapture),
1919
override val otelLimits: OtelLimitsConfig = FakeOtelLimitsConfig(),
20-
override val project: ProjectConfig = FakeProjectConfig(base.project, appId = "abcde"),
20+
override val project: ProjectConfig = FakeProjectConfig(
21+
base = base.project,
22+
appId = "abcde",
23+
versionName = "2.5.1",
24+
packageName = "com.fake.package",
25+
),
2126
override val redaction: RedactionConfig = FakeRedactionConfig(base.redaction),
2227
override val symbols: Base64SharedObjectFilesMap =
2328
FakeBase64SharedObjectFilesMap(base.symbols.getBase64SharedObjectFilesMap()),

embrace-android-config-fakes/src/main/kotlin/io/embrace/android/embracesdk/fakes/config/FakeProjectConfig.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ class FakeProjectConfig(
1111
private val buildType: String? = base.getBuildType(),
1212
private val buildFlavor: String? = base.getBuildFlavor(),
1313
private val packageName: String? = base.getPackageName(),
14+
private val versionName: String? = base.getVersionName(),
1415
) : ProjectConfig {
1516
override fun getAppId(): String? = appId
1617
override fun getAppFramework(): String? = appFramework
1718
override fun getBuildId(): String? = buildId
1819
override fun getBuildType(): String? = buildType
1920
override fun getBuildFlavor(): String? = buildFlavor
2021
override fun getPackageName(): String? = packageName
22+
override fun getVersionName(): String? = versionName
2123
}

embrace-android-config/src/main/kotlin/io/embrace/android/embracesdk/internal/config/behavior/OtelBehavior.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,9 @@ interface OtelBehavior {
1010
* Returns true if the Kotlin SDK should be used, false if it was disabled via remote config.
1111
*/
1212
fun shouldUseKotlinSdk(): Boolean
13+
14+
/**
15+
* Whether resource attributes that the Embrace SDK sets itself can be overridden. Defaults to false.
16+
*/
17+
fun isResourceAttributeOverrideEnabled(): Boolean
1318
}

embrace-android-config/src/main/kotlin/io/embrace/android/embracesdk/internal/config/behavior/OtelBehaviorImpl.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ class OtelBehaviorImpl(
1818
override fun shouldUseKotlinSdk(): Boolean {
1919
return thresholdCheck.isBehaviorEnabled(remote?.pctEnabled) ?: local.isOtelKotlinSdkEnabled()
2020
}
21+
22+
override fun isResourceAttributeOverrideEnabled(): Boolean = local.isResourceAttributeOverrideEnabled()
2123
}

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/metadata/EmbraceMetadataService.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import io.embrace.android.embracesdk.internal.config.ConfigService
1515
import io.embrace.android.embracesdk.internal.envelope.resource.EnvelopeResourceSource
1616
import io.embrace.android.embracesdk.internal.store.KeyValueStore
1717
import io.embrace.android.embracesdk.internal.worker.BackgroundWorker
18+
import io.opentelemetry.kotlin.semconv.OsAttributes
19+
import io.opentelemetry.kotlin.semconv.ServiceAttributes
20+
import kotlinx.serialization.json.contentOrNull
1821

1922
/**
2023
* Provides information about the state of the device, retrieved from Android system services,
@@ -53,8 +56,8 @@ internal class EmbraceMetadataService(
5356
*/
5457
override fun precomputeValues() {
5558
metadataBackgroundWorker.submit {
56-
appVersion = res.appVersion
57-
osVersion = res.osVersion
59+
appVersion = res.attributes[ServiceAttributes.SERVICE_VERSION]?.contentOrNull
60+
osVersion = res.attributes[OsAttributes.OS_VERSION]?.contentOrNull
5861
val free = statFs.freeBytes
5962
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && configService.autoDataCaptureBehavior.isDiskUsageCaptureEnabled()) {
6063
val deviceDiskAppUsage = getDeviceDiskAppUsage(

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/OpenTelemetryModuleImpl.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class OpenTelemetryModuleImpl(
5858
sessionIdsProvider = { storedSessionIdsProvider },
5959
userIdProvider = { storedUserIdProvider?.invoke() },
6060
processIdentifierProvider = processIdentifierProvider,
61+
resourceAttributeOverrideEnabled = { otelBehavior?.isResourceAttributeOverrideEnabled() ?: false },
6162
)
6263
}
6364

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/PayloadSourceModuleImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ class PayloadSourceModuleImpl(
125125
)
126126
},
127127
rnBundleIdProvider = { rnBundleIdTracker.getReactNativeBundleId() },
128-
versionName = BuildConfig.VERSION_NAME,
129128
versionCode = BuildConfig.VERSION_CODE.toIntOrNull(),
129+
otelResourceAttributesProvider = { otelModule.otelSdkConfig.getResourceAttributes() },
130130
)
131131
}
132132
}
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,65 @@
11
package io.embrace.android.embracesdk.internal.envelope.resource
22

3-
import android.os.Environment
43
import io.embrace.android.embracesdk.fakes.FakeConfigService
54
import io.embrace.android.embracesdk.fakes.FakeDevice
6-
import io.embrace.android.embracesdk.fakes.FakeKeyValueStore
5+
import io.embrace.android.embracesdk.fakes.FakeHostedSdkVersionInfo
76
import io.embrace.android.embracesdk.internal.capture.metadata.AppEnvironment
8-
import io.embrace.android.embracesdk.internal.envelope.metadata.UnitySdkVersionInfo
9-
import io.embrace.android.embracesdk.internal.payload.AppFramework
10-
import io.mockk.every
11-
import io.mockk.mockkStatic
12-
import io.mockk.unmockkAll
13-
import org.junit.After
7+
import io.opentelemetry.kotlin.semconv.ServiceAttributes
148
import org.junit.Assert.assertEquals
15-
import org.junit.BeforeClass
9+
import org.junit.Assert.assertFalse
1610
import org.junit.Test
17-
import java.io.File
1811

1912
internal class EnvelopeResourceSourceImplTest {
2013

21-
companion object {
22-
@BeforeClass
23-
@JvmStatic
24-
fun beforeClass() {
25-
mockkStatic(Environment::class)
26-
every { Environment.getDataDirectory() }.returns(File("ANDROID_DATA"))
27-
}
14+
@Test
15+
fun `getEnvelopeResource merges the OTel resource with Embrace internal attributes`() {
16+
val source = createSource(
17+
otelResourceAttributes = mapOf(
18+
ServiceAttributes.SERVICE_VERSION to "2.5.1",
19+
"my.custom.one" to "1",
20+
"build_id" to "should-be-ignored",
21+
),
22+
)
23+
val attrs = source.getEnvelopeResource().attributes
2824

29-
@After
30-
fun tearDown() {
31-
unmockkAll()
32-
}
25+
assertEquals("2.5.1", attrs.getValue(ServiceAttributes.SERVICE_VERSION).content)
26+
assertEquals("1", attrs.getValue("my.custom.one").content)
27+
assertEquals("fakeBuildId", attrs.getValue("build_id").content)
28+
assertEquals("prod", attrs.getValue("emb.app.environment").content)
29+
30+
// legacy bespoke keys are gone, replaced by their canonical semconv keys
31+
assertFalse(attrs.containsKey("app_version"))
32+
assertFalse(attrs.containsKey("app_ecosystem_id"))
33+
assertFalse(attrs.containsKey("environment"))
34+
assertFalse(attrs.containsKey("sdk_version"))
35+
assertFalse(attrs.containsKey("device_manufacturer"))
36+
assertFalse(attrs.containsKey("device_model"))
3337
}
3438

3539
@Test
36-
fun getEnvelopeResource() {
37-
val hostedSdkVersionInfo = UnitySdkVersionInfo(FakeKeyValueStore())
38-
hostedSdkVersionInfo.hostedSdkVersion = "1.2.0"
39-
hostedSdkVersionInfo.hostedPlatformVersion = "19"
40-
hostedSdkVersionInfo.unityBuildIdNumber = "5092abc"
41-
val source = EnvelopeResourceSourceImpl(
42-
FakeConfigService(),
43-
hostedSdkVersionInfo,
44-
AppEnvironment.Environment.PROD,
45-
FakeDevice(),
46-
"",
47-
53,
48-
{ "fakeReactNativeBundleId" },
49-
)
50-
val envelope = source.getEnvelopeResource()
40+
fun `internal attributes added via add() are not overridable by customer attributes`() {
41+
val source = createSource(
42+
otelResourceAttributes = mapOf(
43+
"my.internal.key" to "customer-value",
44+
"other.custom" to "keep",
45+
),
46+
).apply {
47+
add("my.internal.key", "embrace-value")
48+
}
49+
val attrs = source.getEnvelopeResource().attributes
5150

52-
assertEquals("2.5.1", envelope.appVersion)
53-
assertEquals(AppFramework.NATIVE, envelope.appFramework)
54-
assertEquals("com.fake.package", envelope.appEcosystemId)
55-
assertEquals("fakeBuildId", envelope.buildId)
56-
assertEquals("fakeBuildType", envelope.buildType)
57-
assertEquals("fakeBuildFlavor", envelope.buildFlavor)
58-
assertEquals("prod", envelope.environment)
59-
assertEquals("99", envelope.bundleVersion)
60-
assertEquals(53, envelope.sdkSimpleVersion)
61-
assertEquals("fakeReactNativeBundleId", envelope.reactNativeBundleId)
62-
assertEquals("1.2.0", envelope.hostedSdkVersion)
63-
assertEquals("19", envelope.hostedPlatformVersion)
64-
assertEquals("5092abc", envelope.unityBuildId)
65-
assertEquals("Samsung", envelope.deviceManufacturer)
66-
assertEquals("Galaxy S10", envelope.deviceModel)
67-
assertEquals("arm64-v8a", envelope.deviceArchitecture)
68-
assertEquals(false, envelope.jailbroken)
69-
assertEquals(10000000L, envelope.diskTotalCapacity)
70-
assertEquals("linux", envelope.osType)
71-
assertEquals("android", envelope.osName)
72-
assertEquals("8.0.0", envelope.osVersion)
73-
assertEquals("26", envelope.osCode)
74-
assertEquals("1920x1080", envelope.screenResolution)
75-
assertEquals(8, envelope.numCores)
51+
assertEquals("embrace-value", attrs.getValue("my.internal.key").content)
52+
assertEquals("keep", attrs.getValue("other.custom").content)
7653
}
54+
55+
private fun createSource(otelResourceAttributes: Map<String, String>) =
56+
EnvelopeResourceSourceImpl(
57+
configService = FakeConfigService(),
58+
hosted = FakeHostedSdkVersionInfo(),
59+
environment = AppEnvironment.Environment.PROD,
60+
device = FakeDevice(),
61+
versionCode = 53,
62+
rnBundleIdProvider = { null },
63+
otelResourceAttributesProvider = { otelResourceAttributes },
64+
)
7765
}

0 commit comments

Comments
 (0)