Skip to content

Commit 8a9de5a

Browse files
authored
test: O11Y-497 - Sampling E2E testing (#227)
## Summary - End-to-end tests for the LaunchDarkly Observability SDK's sampling functionality. ## How did you test this change? - This work is actually an E2E test, which uses Robolectric to emulate the Android operating system and MockWebServer to mock http request responses. ## Are there any deployment considerations? No
1 parent 06226ca commit 8a9de5a

File tree

19 files changed

+740
-69
lines changed

19 files changed

+740
-69
lines changed

e2e/android/app/build.gradle.kts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ android {
3737
buildFeatures {
3838
compose = true
3939
}
40+
packaging {
41+
resources {
42+
excludes += "/META-INF/LICENSE.md"
43+
excludes += "/META-INF/LICENSE-notice.md"
44+
}
45+
}
46+
testOptions {
47+
unitTests.isIncludeAndroidResources = true
48+
}
4049
}
4150

4251
dependencies {
@@ -70,11 +79,19 @@ dependencies {
7079
implementation(libs.androidx.ui.graphics)
7180
implementation(libs.androidx.ui.tooling.preview)
7281
implementation(libs.androidx.material3)
82+
7383
testImplementation(libs.junit)
84+
testImplementation(libs.androidx.ui.test.junit4)
85+
testImplementation(libs.core.ktx)
86+
testImplementation(libs.robolectric)
87+
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
88+
testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.51.0")
89+
7490
androidTestImplementation(libs.androidx.junit)
7591
androidTestImplementation(libs.androidx.espresso.core)
7692
androidTestImplementation(platform(libs.androidx.compose.bom))
7793
androidTestImplementation(libs.androidx.ui.test.junit4)
94+
7895
debugImplementation(libs.androidx.ui.tooling)
7996
debugImplementation(libs.androidx.ui.test.manifest)
8097
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
3+
<application
4+
android:networkSecurityConfig="@xml/network_security_config"
5+
android:usesCleartextTraffic="true" />
6+
</manifest>
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
{
2+
"data": {
3+
"sampling": {
4+
"logs": [
5+
{
6+
"severityText": {
7+
"matchValue": "ERROR"
8+
},
9+
"samplingRatio": 0
10+
},
11+
{
12+
"message": {
13+
"matchValue": "Connection failed"
14+
},
15+
"samplingRatio": 0
16+
},
17+
{
18+
"message": {
19+
"regexValue": "Error: .*"
20+
},
21+
"samplingRatio": 0
22+
},
23+
{
24+
"message": {
25+
"regexValue": "Database connection .*"
26+
},
27+
"severityText": {
28+
"matchValue": "WARN"
29+
},
30+
"attributes": [
31+
{
32+
"key": {
33+
"regexValue": "service.*"
34+
},
35+
"attribute": {
36+
"regexValue": "db-.*"
37+
}
38+
},
39+
{
40+
"key": {
41+
"matchValue": "retry.enabled"
42+
},
43+
"attribute": {
44+
"matchValue": true
45+
}
46+
},
47+
{
48+
"key": {
49+
"matchValue": "retry.count"
50+
},
51+
"attribute": {
52+
"matchValue": 3
53+
}
54+
},
55+
{
56+
"key": {
57+
"matchValue": "retry.timeout"
58+
},
59+
"attribute": {
60+
"matchValue": 15.5
61+
}
62+
}
63+
],
64+
"samplingRatio": 0
65+
}
66+
],
67+
"spans": [
68+
{
69+
"name": {
70+
"matchValue": "test-span"
71+
},
72+
"samplingRatio": 0
73+
},
74+
{
75+
"name": {
76+
"regexValue": "test-span-\\d+"
77+
},
78+
"samplingRatio": 0
79+
},
80+
{
81+
"events": [
82+
{
83+
"name": {
84+
"matchValue": "test-event"
85+
}
86+
}
87+
],
88+
"samplingRatio": 0
89+
},
90+
{
91+
"events": [
92+
{
93+
"name": {
94+
"regexValue": "test-event-\\d+"
95+
}
96+
}
97+
],
98+
"samplingRatio": 0
99+
},
100+
{
101+
"attributes": [
102+
{
103+
"key": {
104+
"matchValue": "http.method"
105+
},
106+
"attribute": {
107+
"matchValue": "POST"
108+
}
109+
}
110+
],
111+
"samplingRatio": 0
112+
},
113+
{
114+
"events": [
115+
{
116+
"attributes": [
117+
{
118+
"key": {
119+
"matchValue": "error.type"
120+
},
121+
"attribute": {
122+
"matchValue": "network"
123+
}
124+
},
125+
{
126+
"key": {
127+
"matchValue": "db.error"
128+
},
129+
"attribute": {
130+
"regexValue": "Database connection .*"
131+
}
132+
},
133+
{
134+
"key": {
135+
"matchValue": "error.code"
136+
},
137+
"attribute": {
138+
"matchValue": 503
139+
}
140+
}
141+
]
142+
}
143+
],
144+
"samplingRatio": 0
145+
}
146+
]
147+
}
148+
}
149+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<network-security-config>
3+
<domain-config cleartextTrafficPermitted="true">
4+
<domain includeSubdomains="true">127.0.0.1</domain>
5+
<domain includeSubdomains="true">localhost</domain>
6+
</domain-config>
7+
</network-security-config>

e2e/android/app/src/main/java/com/example/androidobservability/BaseApplication.kt

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.example.androidobservability
22

33
import android.app.Application
44
import com.launchdarkly.observability.api.Options
5+
import com.launchdarkly.observability.client.TelemetryInspector
56
import com.launchdarkly.sdk.ContextKind
67
import com.launchdarkly.sdk.LDContext
78
import com.launchdarkly.sdk.android.Components
@@ -14,17 +15,33 @@ import io.opentelemetry.api.common.AttributeKey
1415
import io.opentelemetry.api.common.Attributes
1516
import java.util.Collections
1617

17-
class BaseApplication : Application() {
18+
open class BaseApplication : Application() {
1819

1920
companion object {
2021
// TODO O11Y-376: Update this credential to be driven by env variable or gradle property
2122
// Set LAUNCHDARKLY_MOBILE_KEY to your LaunchDarkly SDK mobile key.
2223
const val LAUNCHDARKLY_MOBILE_KEY = "MOBILE_KEY_GOES_HERE"
2324
}
2425

26+
var telemetryInspector: TelemetryInspector? = null
27+
2528
override fun onCreate() {
2629
super.onCreate()
2730

31+
val testUrl = System.getProperty("e2e_test_base_url")
32+
val pluginOptions = Options(
33+
resourceAttributes = Attributes.of(
34+
AttributeKey.stringKey("example"), "value"
35+
),
36+
debug = true,
37+
logAdapter = LDAndroidLogging.adapter(),
38+
)
39+
40+
val observabilityPlugin = Observability(
41+
application = this@BaseApplication,
42+
options = if (testUrl != null) pluginOptions.copy(backendUrl = testUrl) else pluginOptions
43+
)
44+
2845
// Set LAUNCHDARKLY_MOBILE_KEY to your LaunchDarkly mobile key found on the LaunchDarkly
2946
// dashboard in the start guide.
3047
// If you want to disable the Auto EnvironmentAttributes functionality.
@@ -33,18 +50,7 @@ class BaseApplication : Application() {
3350
.mobileKey(LAUNCHDARKLY_MOBILE_KEY)
3451
.plugins(
3552
Components.plugins().setPlugins(
36-
Collections.singletonList<Plugin>(
37-
Observability(
38-
this@BaseApplication,
39-
Options(
40-
resourceAttributes = Attributes.of(
41-
AttributeKey.stringKey("example"), "value"
42-
),
43-
debug = true,
44-
logAdapter = LDAndroidLogging.adapter(),
45-
)
46-
)
47-
)
53+
Collections.singletonList<Plugin>(observabilityPlugin)
4854
)
4955
)
5056
.build()
@@ -56,5 +62,7 @@ class BaseApplication : Application() {
5662
.build()
5763

5864
LDClient.init(this@BaseApplication, ldConfig, context)
65+
66+
telemetryInspector = observabilityPlugin.getTelemetryInspector()
5967
}
6068
}

e2e/android/app/src/main/java/com/example/androidobservability/MainActivity.kt

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,58 @@ import androidx.activity.compose.setContent
77
import androidx.activity.enableEdgeToEdge
88
import androidx.activity.viewModels
99
import androidx.compose.foundation.layout.Column
10+
import androidx.compose.foundation.layout.Spacer
1011
import androidx.compose.foundation.layout.fillMaxSize
12+
import androidx.compose.foundation.layout.height
1113
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.rememberScrollState
15+
import androidx.compose.foundation.verticalScroll
1216
import androidx.compose.material3.Button
17+
import androidx.compose.material3.OutlinedTextField
1318
import androidx.compose.material3.Scaffold
1419
import androidx.compose.material3.Text
15-
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.getValue
21+
import androidx.compose.runtime.mutableStateOf
22+
import androidx.compose.runtime.remember
23+
import androidx.compose.runtime.setValue
1624
import androidx.compose.ui.Modifier
17-
import androidx.compose.ui.tooling.preview.Preview
25+
import androidx.compose.ui.unit.dp
1826
import com.example.androidobservability.ui.theme.AndroidObservabilityTheme
1927

2028
class MainActivity : ComponentActivity() {
29+
2130
override fun onCreate(savedInstanceState: Bundle?) {
2231
super.onCreate(savedInstanceState)
32+
2333
val viewModel: ViewModel by viewModels()
34+
2435
enableEdgeToEdge()
2536
setContent {
2637
AndroidObservabilityTheme {
2738
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
28-
Column {
39+
var customLogText by remember { mutableStateOf("") }
40+
var customSpanText by remember { mutableStateOf("") }
41+
42+
Column(
43+
modifier = Modifier
44+
.padding(innerPadding)
45+
.padding(16.dp)
46+
.verticalScroll(rememberScrollState())
47+
) {
48+
2949
Text(
3050
text = "Hello Telemetry",
31-
modifier = Modifier.padding(innerPadding)
51+
modifier = Modifier.padding(bottom = 16.dp)
3252
)
53+
3354
Button(
3455
onClick = {
35-
this@MainActivity.startActivity(Intent(this@MainActivity, SecondaryActivity::class.java))
56+
this@MainActivity.startActivity(
57+
Intent(
58+
this@MainActivity,
59+
SecondaryActivity::class.java
60+
)
61+
)
3662
}
3763
) {
3864
Text("Go to Secondary Activity")
@@ -79,25 +105,41 @@ class MainActivity : ComponentActivity() {
79105
) {
80106
Text("Trigger Crash")
81107
}
108+
109+
OutlinedTextField(
110+
value = customLogText,
111+
onValueChange = { customLogText = it },
112+
label = { Text("Log Message") },
113+
modifier = Modifier.padding(8.dp)
114+
)
115+
Button(
116+
onClick = {
117+
viewModel.triggerCustomLog(customLogText)
118+
},
119+
modifier = Modifier.padding(8.dp)
120+
) {
121+
Text("Send custom log")
122+
}
123+
124+
Spacer(modifier = Modifier.height(16.dp))
125+
126+
OutlinedTextField(
127+
value = customSpanText,
128+
onValueChange = { customSpanText = it },
129+
label = { Text("Span Name") },
130+
modifier = Modifier.padding(8.dp)
131+
)
132+
Button(
133+
onClick = {
134+
viewModel.triggerCustomSpan(customSpanText)
135+
},
136+
modifier = Modifier.padding(8.dp)
137+
) {
138+
Text("Send custom span")
139+
}
82140
}
83141
}
84142
}
85143
}
86144
}
87145
}
88-
89-
@Composable
90-
fun Greeting(name: String, modifier: Modifier = Modifier) {
91-
Text(
92-
text = "Hello $name!",
93-
modifier = modifier
94-
)
95-
}
96-
97-
@Preview(showBackground = true)
98-
@Composable
99-
fun GreetingPreview() {
100-
AndroidObservabilityTheme {
101-
Greeting("Android")
102-
}
103-
}

0 commit comments

Comments
 (0)